changeset 366:886f66775f31

animone: add preliminary AT-SPI stuff anime_list: finish the regular singular right click menu
author Paper <paper@tflc.us>
date Sun, 17 Nov 2024 19:56:01 -0500
parents f81bed4e04ac
children 8d45d892be88
files dep/animone/CMakeLists.txt dep/animone/include/animone/a11y.h dep/animone/include/animone/a11y/atspi.h dep/animone/include/animone/a11y/win32.h dep/animone/include/animone/types.h dep/animone/src/a11y.cc dep/animone/src/a11y/atspi.cc dep/animone/src/a11y/win32.cc dep/animone/src/strategist.cc include/library/library.h src/gui/pages/anime_list.cc src/gui/window.cc src/library/library.cc src/track/media.cc
diffstat 14 files changed, 310 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/dep/animone/CMakeLists.txt	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/CMakeLists.txt	Sun Nov 17 19:56:01 2024 -0500
@@ -87,6 +87,14 @@
 		list(APPEND INCLUDE_DIRS ${XCB_INCLUDE_DIRS})
 		list(APPEND SRC_FILES src/win/x11.cc)
 	endif() # XCB_FOUND
+
+	pkg_check_modules(ATSPI atspi-2)
+	if (ATSPI_FOUND)
+		list(APPEND DEFINES USE_ATSPI)
+		list(APPEND LIBRARIES ${ATSPI_LINK_LIBRARIES})
+		list(APPEND INCLUDE_DIRS ${ATSPI_INCLUDE_DIRS})
+		list(APPEND SRC_FILES src/a11y/atspi.cc)
+	endif() # ATSPI_FOUND
 endif() # PKG_CONFIG_FOUND
 
 add_library(animia SHARED ${SRC_FILES})
--- a/dep/animone/include/animone/a11y.h	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/include/animone/a11y.h	Sun Nov 17 19:56:01 2024 -0500
@@ -22,7 +22,7 @@
 
 using web_browser_proc_t = std::function<void(const WebBrowserInformation&)>;
 
-bool GetWebBrowserInformation(const Window& window, web_browser_proc_t web_browser_proc);
+bool GetWebBrowserInformation(const Result& result, web_browser_proc_t web_browser_proc);
 
 } // namespace internal
 } // namespace animone
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dep/animone/include/animone/a11y/atspi.h	Sun Nov 17 19:56:01 2024 -0500
@@ -0,0 +1,13 @@
+#ifndef ANIMONE_ANIMONE_A11Y_ATSPI_H_
+#define ANIMONE_ANIMONE_A11Y_ATSPI_H_
+
+#include "animone.h"
+#include "animone/a11y.h"
+
+namespace animone::internal::atspi {
+
+bool GetWebBrowserInformation(const Result& result, web_browser_proc_t web_browser_proc);
+
+} // namespace animone::internal::atspi
+
+#endif // ANIMONE_ANIMONE_A11Y_ATSPI_H_
--- a/dep/animone/include/animone/a11y/win32.h	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/include/animone/a11y/win32.h	Sun Nov 17 19:56:01 2024 -0500
@@ -7,7 +7,7 @@
 
 namespace animone::internal::win32 {
 
-bool GetWebBrowserInformation(const Window& window, web_browser_proc_t web_browser_proc);
+bool GetWebBrowserInformation(const Result& result, web_browser_proc_t web_browser_proc);
 
 } // namespace animone::internal::win32
 
--- a/dep/animone/include/animone/types.h	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/include/animone/types.h	Sun Nov 17 19:56:01 2024 -0500
@@ -29,7 +29,7 @@
 
 /* different window systems have different sized IDs */
 union ANIMONE_API wid_t {
-	std::uintptr_t win32;
+	std::uintptr_t win32; // XXX this ought to be a `void *`
 	std::int64_t quartz; // FIXME is this correct?
 	std::uint32_t x11;
 };
--- a/dep/animone/src/a11y.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/src/a11y.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -4,13 +4,21 @@
 #	include "animone/a11y/win32.h"
 #endif
 
+#ifdef USE_ATSPI
+#	include "animone/a11y/atspi.h"
+#endif
+
 namespace animone::internal {
 
-bool GetWebBrowserInformation(const Window& window, web_browser_proc_t web_browser_proc) {
+bool GetWebBrowserInformation(const Result& result, web_browser_proc_t web_browser_proc) {
 	bool success = false;
 
 #ifdef USE_WIN32
-	success ^= win32::GetWebBrowserInformation(window, web_browser_proc);
+	success ^= win32::GetWebBrowserInformation(result, web_browser_proc);
+#endif
+
+#ifdef USE_ATSPI
+	success ^= atspi::GetWebBrowserInformation(result, web_browser_proc);
 #endif
 
 	return success;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dep/animone/src/a11y/atspi.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -0,0 +1,77 @@
+#include <functional>
+#include <string>
+#include <vector>
+#include <memory>
+
+#include "animone.h"
+#include "animone/a11y/atspi.h"
+
+#include <atspi/atspi.h>
+
+namespace animone::internal::atspi {
+
+/* deleters */
+template<typename T>
+struct g_object_del {
+    void operator()(T* p) const { ::g_object_unref(p); };
+};
+
+template<typename T>
+using GObjectPtr = std::unique_ptr<T, g_object_del<T>>;
+
+/* ----------------------------------------------------------------- */
+
+// FIXME | atspi_exit()
+bool GetWebBrowserInformation(const Result& result, web_browser_proc_t web_browser_proc) {
+	GObjectPtr<AtspiAccessible> desktop;
+	GObjectPtr<AtspiAccessible> application;
+
+	{
+		int err = atspi_init();
+		if (err != 0 && err != 1)
+			return false;
+	}
+
+	// Currently only one desktop is supported, so this is equivalent to doing
+	// just atspi_get_desktop(0). However it's nice to futureproof where possible.
+	for (gint i = 0; i < atspi_get_desktop_count(); i++) {
+		desktop.reset(atspi_get_desktop(i));
+		if (!desktop)
+			return false;
+
+		for (gint j = 0; j < atspi_accessible_get_child_count(application.get(), nullptr); j++) {
+			application.reset(atspi_accessible_get_child_at_index(desktop.get(), j, nullptr));
+			if (!application)
+				return false;
+
+			GError *error = NULL;
+
+			std::uint32_t pid = atspi_accessible_get_process_id(application.get(), &error);
+			if (error) {
+				::g_error_free(error);
+				return false;
+			}
+
+			if (pid == result.process.pid)
+				goto found; // found it
+		}
+	}
+
+	// didn't get anything... lol
+	return false;
+
+found:
+	// found a matching application
+
+	gchar *title = atspi_accessible_get_name(application.get(), NULL);
+	if (title) {
+		web_browser_proc({WebBrowserInformationType::Title, title});
+		::g_free(title);
+	}
+
+	// TODO need to find address and tab? idk
+
+	return true;
+}
+
+}
--- a/dep/animone/src/a11y/win32.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/src/a11y/win32.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -235,14 +235,14 @@
 
 /* ------------------------------------------------------------------------------------ */
 
-bool GetWebBrowserInformation(const Window& window, web_browser_proc_t web_browser_proc) {
+bool GetWebBrowserInformation(const Result& result, web_browser_proc_t web_browser_proc) {
 	if (!web_browser_proc)
 		return false;
 
 	if (!InitializeUIAutomation())
 		return false;
 
-	ComInterface<Element> parent(GetElementFromHandle(reinterpret_cast<HWND>(window.id.win32)));
+	ComInterface<Element> parent(GetElementFromHandle(reinterpret_cast<HWND>(result.window.id.win32)));
 	if (!parent)
 		return false;
 
--- a/dep/animone/src/strategist.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/dep/animone/src/strategist.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -122,7 +122,7 @@
 			}
 		};
 
-		success |= GetWebBrowserInformation(result.window, web_browser_proc);
+		success |= GetWebBrowserInformation(result, web_browser_proc);
 	}
 
 	return success;
--- a/include/library/library.h	Wed Oct 02 23:06:43 2024 -0400
+++ b/include/library/library.h	Sun Nov 17 19:56:01 2024 -0500
@@ -3,16 +3,23 @@
 
 #include "library/library.h"
 
+#include <optional>
 #include <filesystem>
 #include <unordered_map>
 
 namespace Library {
 
-class Database {
+class Database final {
 public:
+	std::optional<std::filesystem::path> GetAnimeFolder(int id);
 	void Refresh();
+	void Refresh(int id);
 
+	// Anime episodes. Indexed as `folders[id][episode]'
 	std::unordered_map<int, std::unordered_map<int, std::filesystem::path>> items;
+
+private:
+	void Refresh(std::optional<int> find_id);
 };
 
 extern Database db;
--- a/src/gui/pages/anime_list.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/src/gui/pages/anime_list.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -14,6 +14,7 @@
 #include "core/session.h"
 #include "core/strings.h"
 #include "core/time.h"
+#include "library/library.h"
 #include "gui/dialog/information.h"
 #include "gui/translate/anime.h"
 #include "services/services.h"
@@ -30,8 +31,11 @@
 #include <QThreadPool>
 #include <QRunnable>
 #include <QTreeView>
+#include <QDesktopServices>
+#include <QUrl>
 
-#include <set>
+#include <iostream>
+#include <vector>
 
 AnimeListPageUpdateEntryThread::AnimeListPageUpdateEntryThread(QObject* parent) : QThread(parent) {}
 
@@ -272,7 +276,7 @@
 			if (checked && (tree_view->columnWidth(i) <= 5))
 				tree_view->resizeColumnToContents(i);
 
-			// SaveSettings();
+			// FIXME save the state of this
 		});
 
 		action->setCheckable(true);
@@ -290,7 +294,7 @@
 }
 
 void AnimeListPage::DisplayListMenu() {
-	QMenu* menu = new QMenu(this);
+	QMenu *const menu = new QMenu(this);
 	menu->setAttribute(Qt::WA_DeleteOnClose);
 	menu->setToolTipsVisible(true);
 
@@ -303,14 +307,71 @@
 	for (const auto& index : selection.indexes()) {
 		if (!index.isValid())
 			continue;
-		Anime::Anime* anime = source_model->GetAnimeFromIndex(index);
+
+		Anime::Anime *const anime = source_model->GetAnimeFromIndex(index);
 		if (!anime)
 			continue;
+
 		animes.insert(&Anime::db.items[anime->GetId()]);
 	}
 
-	menu->addAction(tr("Information"), [this, animes] {
-		for (auto& anime : animes) {
+	if (animes.size() > 1) {
+		// menu in Taiga:
+		//
+		// Set date started ->
+		//   Clear
+		//   Set to date started airing
+		// Set date completed ->
+		//   Clear
+		//   Set to date finished airing
+		//   Set to last updated
+		// Set episode...
+		// Set score ->
+		//   0
+		//   10
+		//   ...
+		//   100
+		// Set status ->
+		//   Currently watching
+		//   ...
+		//   Plan to watch
+		// Set notes...
+		// ----------------
+		// Invert selection
+		// ----------------
+		// Delete from list... <Del>
+	} else if (animes.size() > 0) {
+		// menu in Taiga:
+		//
+		// Information
+		// Search ->
+		//   AniDB
+		//   AniList
+		//   Anime News Network
+		//   Kitsu
+		//   MyAnimeList
+		//   Reddit
+		//   Wikipedia
+		//   YouTube
+		//   ----------------
+		//   Custom RSS feed
+		//   Nyaa.si
+		// ----------------
+		// Edit
+		// Delete from list... <Del>
+		// ----------------
+		// Open folder <Ctrl+O>
+		// Scan available episodes <F5>
+		// ----------------
+		// Play episode ->
+		//   grid of episodes (dunno how to implement this)
+		// Play last episode (#<episode>)
+		// Play next episode (#<episode>) <Ctrl+N>
+		// Play random episode <Ctrl+R> (why?)
+
+		Anime::Anime *anime = *animes.begin();
+
+		menu->addAction(tr("Information"), [this, anime] {
 			InformationDialog* dialog = new InformationDialog(
 				anime, [this](Anime::Anime* anime) { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MAIN_INFO, this);
 
@@ -318,11 +379,11 @@
 			dialog->raise();
 			dialog->activateWindow();
 			connect(dialog, &InformationDialog::finished, dialog, &InformationDialog::deleteLater);
-		}
-	});
-	menu->addSeparator();
-	menu->addAction(tr("Edit"), [this, animes] {
-		for (auto& anime : animes) {
+		});
+
+		menu->addSeparator();
+
+		menu->addAction(tr("Edit"), [this, anime] {
 			InformationDialog* dialog = new InformationDialog(
 				anime, [this](Anime::Anime* anime) { UpdateAnime(anime->GetId()); }, InformationDialog::PAGE_MY_LIST, this);
 
@@ -330,14 +391,80 @@
 			dialog->raise();
 			dialog->activateWindow();
 			connect(dialog, &InformationDialog::finished, dialog, &InformationDialog::deleteLater);
+		});
+		menu->addAction(tr("Delete from list..."), [this, anime] {
+			RemoveAnime(anime->GetId());
+		}, QKeySequence(QKeySequence::Delete));
+
+		menu->addSeparator();
+
+		menu->addAction(tr("Open folder"), [this, anime] {
+			std::optional<std::filesystem::path> path = Library::db.GetAnimeFolder(anime->GetId());
+			if (!path) // ...
+				return;
+
+			QDesktopServices::openUrl(QUrl::fromLocalFile(Strings::ToQString(path.value().u8string())));
+		});
+		menu->addAction(tr("Scan available episodes"), [this, anime] {
+			Library::db.Refresh(anime->GetId());
+		});
+
+		menu->addSeparator();
+
+		{
+			QMenu *submenu = menu->addMenu(tr("Play episode"));
+
+			// this submenu actually uses win32 API magic to
+			// make a *grid* of episodes (weird!)
+
+			(void)submenu;
 		}
-	});
-	menu->addAction(tr("Delete from list..."), [this, animes] {
-		for (auto& anime : animes) {
-			RemoveAnime(anime->GetId());
+
+		const int progress = anime->GetUserProgress();
+		const int episodes = anime->GetEpisodes();
+
+		// I think this is right?
+		if (progress > 0) {
+			menu->addAction(tr("Play last episode (#%1)").arg(progress), [this, anime, progress] {
+				const int id = anime->GetId();
+
+				if (Library::db.items.find(id) == Library::db.items.end()
+					|| Library::db.items[id].find(progress) == Library::db.items[id].end())
+					return;
+
+				QDesktopServices::openUrl(QUrl::fromLocalFile(Strings::ToQString(Library::db.items[id][progress].u8string())));
+			});
 		}
-	});
-	menu->popup(QCursor::pos());
+
+		if (progress < episodes) {
+			menu->addAction(tr("Play next episode (#%1)").arg(progress + 1), [this, anime, progress] {
+				const int id = anime->GetId();
+
+				if (Library::db.items.find(id) == Library::db.items.end()
+					|| Library::db.items[id].find(progress + 1) == Library::db.items[id].end())
+					return;
+
+				QDesktopServices::openUrl(QUrl::fromLocalFile(Strings::ToQString(Library::db.items[id][progress + 1].u8string())));
+			}, QKeySequence(Qt::CTRL | Qt::Key_N));
+		}
+
+		menu->addAction(tr("Play random episode"), [this, anime, episodes] {
+			const int id = anime->GetId();
+
+			std::uniform_int_distribution<int> distrib(1, episodes);
+			const int episode = distrib(session.gen);
+
+			if (Library::db.items.find(id) == Library::db.items.end()
+				|| Library::db.items[id].find(episode) == Library::db.items[id].end())
+				return;
+
+			QDesktopServices::openUrl(QUrl::fromLocalFile(Strings::ToQString(Library::db.items[id][episode].u8string())));
+		}, QKeySequence(Qt::CTRL | Qt::Key_R));
+
+		menu->popup(QCursor::pos());
+	} else {
+		// Where are we now?
+	}
 }
 
 void AnimeListPage::ItemDoubleClicked() {
--- a/src/gui/window.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/src/gui/window.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -140,7 +140,9 @@
 	statusBar()->showMessage(Strings::ToQString(message), 2000);
 }
 
-/* Does the main part of what Qt's generic "RetranslateUI" function would do */
+/* FIXME:
+ * ALL of the pages need to have a retranslate function. This would require
+ * huge amounts of refactoring hence why it hasn't been done yet. */
 void MainWindow::AddMainWidgets() {
 	int page = sidebar_.GetCurrentItem();
 
--- a/src/library/library.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/src/library/library.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -13,7 +13,31 @@
 
 namespace Library {
 
-void Database::Refresh() {
+std::optional<std::filesystem::path> Database::GetAnimeFolder(int id) {
+	// this function sucks, but it's the most I can really do for now.
+	//
+	// in the future the Refresh() function should look for directories
+	// as well that fit the anime name and *also* have episodes in them.
+	// it should give each of these directories a rating by how many
+	// episodes are contained in them. whichever directory has more episodes
+	// wins, or the first found if there is an equal amount.
+
+	for (const auto& [anime_id, episodes] : items) {
+		if (id != anime_id)
+			continue;
+
+		for (const auto& [episode, path] : episodes) {
+			return path.parent_path();
+			break;
+		}
+
+		break;
+	}
+
+	return std::nullopt;
+}
+
+void Database::Refresh(std::optional<int> find_id) {
 	items.clear();
 
 	for (const auto& folder : session.config.library.paths) {
@@ -32,7 +56,7 @@
 			const std::string title = Strings::ToUtf8String(elements.get(anitomy::kElementAnimeTitle));
 
 			const int id = Anime::db.LookupAnimeTitle(title);
-			if (id <= 0)
+			if (id <= 0 || (find_id && find_id.value() != id))
 				continue;
 
 			const int episode = Strings::ToInt<int>(Strings::ToUtf8String(elements.get(anitomy::kElementEpisodeNumber)));
@@ -43,6 +67,16 @@
 	}
 }
 
+void Database::Refresh() {
+	Refresh(std::nullopt);
+}
+
+void Database::Refresh(int id) {
+	Refresh(std::optional<int>(id));
+}
+
+// TODO export to JSON
+
 Database db;
 
 } // namespace Library
--- a/src/track/media.cc	Wed Oct 02 23:06:43 2024 -0400
+++ b/src/track/media.cc	Sun Nov 17 19:56:01 2024 -0500
@@ -29,7 +29,11 @@
 	return animone::GetResults(players, results);
 }
 
-/* meh */
+/* The results from this function are KIND OF guaranteed to be UTF-8;
+ * more specifically any files returned are UTF-8 as required by the C++
+ * standard. However, window titles are not, and for some obscure X11
+ * window managers, WILL not be in UTF-8. I don't care enough about this
+ * to do anything about it though. */
 bool GetCurrentlyPlaying(std::vector<std::string>& vec) {
 	std::vector<animone::Result> results;