diff options
| author | frosty <gabriel@bwaaa.monster> | 2026-08-25 22:50:10 -0400 |
|---|---|---|
| committer | frosty <gabriel@bwaaa.monster> | 2026-08-25 22:50:10 -0400 |
| commit | b19412a3d291f8cdbf037e00ac2f3641f7493de6 (patch) | |
| tree | 8179bc8a64c2286fde58ce829f774ac3d0a32c1b /main/src/ui | |
| parent | d2c69cfb544c23042652dee9ebf0e7031d6c2b29 (diff) | |
feat: add tray icon
Diffstat (limited to 'main/src/ui')
| -rw-r--r-- | main/src/ui/application.vala | 17 | ||||
| -rw-r--r-- | main/src/ui/status_notifier_item.vala | 206 | ||||
| -rw-r--r-- | main/src/ui/status_notifier_menu.vala | 100 |
3 files changed, 321 insertions, 2 deletions
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index 1b4dc049..9ac512d9 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -14,6 +14,7 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { public MainWindow window; public MainWindowController controller; + private StatusNotifierItem? status_notifier_item; public Database db { get; set; } public Dino.Entities.Settings settings { get; set; } @@ -43,6 +44,12 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { Process.exit(0); } + try { + status_notifier_item = new StatusNotifierItem(this); + } catch (IOError e) { + warning("Could not create Dino's tray icon: %s", e.message); + } + NotificationEvents notification_events = stream_interactor.get_module(NotificationEvents.IDENTITY); get_notifications_dbus.begin((_, res) => { // It might take a bit to get the interface. NotificationEvents will queue any notifications in the meantime. @@ -68,13 +75,20 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { stream_interactor.get_module(FileManager.IDENTITY).add_metadata_provider(new Util.AudioVideoFileMetadataProvider()); }); + shutdown.connect(() => { + if (status_notifier_item != null) { + status_notifier_item.shutdown(); + status_notifier_item = null; + } + }); + activate.connect(() => { if (window == null) { controller = new MainWindowController(this, stream_interactor, db); config = new Config(db); window = new MainWindow(this, stream_interactor, db, config); controller.set_window(window); - if ((get_flags() & ApplicationFlags.IS_SERVICE) == ApplicationFlags.IS_SERVICE) window.hide_on_close = true; + window.hide_on_close = true; } window.present(); }); @@ -305,4 +319,3 @@ public class Dino.Ui.Application : Adw.Application, Dino.Application { dialog.present(); } } - diff --git a/main/src/ui/status_notifier_item.vala b/main/src/ui/status_notifier_item.vala new file mode 100644 index 00000000..de29509d --- /dev/null +++ b/main/src/ui/status_notifier_item.vala @@ -0,0 +1,206 @@ +using GLib; + +namespace Dino.Ui { + +public struct StatusNotifierIconPixmap { + public int width; + public int height; + public uint8[] data; +} + +public struct StatusNotifierToolTip { + public string icon_name; + public StatusNotifierIconPixmap[] icon_pixmap; + public string title; + public string description; +} + +[DBus (name = "org.kde.StatusNotifierItem")] +public class StatusNotifierItem : Object { + private const string BUS_NAME_TEMPLATE = "org.kde.StatusNotifierItem-%d-1"; + private const string ICON_NAME = "im.dino.Dino"; + private const string ITEM_OBJECT_PATH = "/StatusNotifierItem"; + private const string MENU_OBJECT_PATH = "/MenuBar"; + private const string WATCHER_BUS_NAME = "org.kde.StatusNotifierWatcher"; + private const string WATCHER_INTERFACE = "org.kde.StatusNotifierWatcher"; + private const string WATCHER_OBJECT_PATH = "/StatusNotifierWatcher"; + + private weak Application application; + private DBusConnection connection; + private string bus_name; + private bool name_acquired; + private bool registration_in_progress; + private bool shutting_down; + private bool watcher_available; + private bool registered_with_watcher; + private string? activation_token; + private string? watcher_owner; + private Cancellable registration_cancellable = new Cancellable(); + private StatusNotifierMenu status_notifier_menu; + private uint bus_name_owner_id; + private uint menu_registration_id; + private uint object_registration_id; + private uint watcher_id; + + public string category { owned get { return "Communications"; } } + public string id { owned get { return "Dino"; } } + public string title { owned get { return "Dino"; } } + public string status { owned get { return "Active"; } } + public int window_id { get { return 0; } } + public string icon_theme_path { owned get { return ""; } } + public ObjectPath menu { owned get { return new ObjectPath(MENU_OBJECT_PATH); } } + public bool item_is_menu { get { return false; } } + public string icon_name { owned get { return ICON_NAME; } } + public StatusNotifierIconPixmap[] icon_pixmap { owned get { return new StatusNotifierIconPixmap[0]; } } + public string overlay_icon_name { owned get { return ""; } } + public StatusNotifierIconPixmap[] overlay_icon_pixmap { owned get { return new StatusNotifierIconPixmap[0]; } } + public string attention_icon_name { owned get { return ""; } } + public StatusNotifierIconPixmap[] attention_icon_pixmap { owned get { return new StatusNotifierIconPixmap[0]; } } + public string attention_movie_name { owned get { return ""; } } + public StatusNotifierToolTip tool_tip { + owned get { + StatusNotifierToolTip tool_tip = StatusNotifierToolTip(); + tool_tip.icon_name = ICON_NAME; + tool_tip.icon_pixmap = new StatusNotifierIconPixmap[0]; + tool_tip.title = "Dino"; + tool_tip.description = _("Open Dino"); + return tool_tip; + } + } + + public signal void new_title(); + public signal void new_icon(); + public signal void new_attention_icon(); + public signal void new_overlay_icon(); + public signal void new_menu(); + public signal void new_tool_tip(); + public signal void new_status(string status); + + public StatusNotifierItem(Application application) throws IOError { + this.application = application; + this.connection = Bus.get_sync(BusType.SESSION); + this.bus_name = BUS_NAME_TEMPLATE.printf((int) Posix.getpid()); + + status_notifier_menu = new StatusNotifierMenu(application); + menu_registration_id = connection.register_object(MENU_OBJECT_PATH, status_notifier_menu); + object_registration_id = connection.register_object(ITEM_OBJECT_PATH, this); + bus_name_owner_id = Bus.own_name_on_connection( + connection, + bus_name, + BusNameOwnerFlags.NONE, + () => { + name_acquired = true; + register_with_watcher.begin(); + }, + () => { + name_acquired = false; + registered_with_watcher = false; + }); + watcher_id = Bus.watch_name_on_connection( + connection, + WATCHER_BUS_NAME, + BusNameWatcherFlags.NONE, + (watcher_connection, watcher_name, name_owner) => { + watcher_owner = name_owner; + watcher_available = true; + registered_with_watcher = false; + register_with_watcher.begin(); + }, + () => { + watcher_owner = null; + watcher_available = false; + registered_with_watcher = false; + }); + } + + internal void shutdown() { + shutting_down = true; + name_acquired = false; + watcher_available = false; + registration_cancellable.cancel(); + + if (watcher_id != 0) { + Bus.unwatch_name(watcher_id); + watcher_id = 0; + } + if (bus_name_owner_id != 0) { + Bus.unown_name(bus_name_owner_id); + bus_name_owner_id = 0; + } + if (object_registration_id != 0) { + connection.unregister_object(object_registration_id); + object_registration_id = 0; + } + if (menu_registration_id != 0) { + connection.unregister_object(menu_registration_id); + menu_registration_id = 0; + } + } + + public void provide_xdg_activation_token(string token) throws Error { + activation_token = token; + } + + public void context_menu(int x, int y) throws Error { + } + + public void activate(int x, int y) throws Error { + show_window(); + } + + public void secondary_activate(int x, int y) throws Error { + show_window(); + } + + public void scroll(int delta, string orientation) throws Error { + } + + private async void register_with_watcher() { + if (shutting_down || !name_acquired || !watcher_available || + registered_with_watcher || registration_in_progress) return; + + registration_in_progress = true; + string? registering_watcher = watcher_owner; + + try { + yield connection.call( + WATCHER_BUS_NAME, + WATCHER_OBJECT_PATH, + WATCHER_INTERFACE, + "RegisterStatusNotifierItem", + new Variant("(s)", bus_name), + null, + DBusCallFlags.NONE, + 5000, + registration_cancellable); + if (!shutting_down && name_acquired && watcher_owner == registering_watcher) { + registered_with_watcher = true; + } + } catch (Error e) { + if (!(e is IOError.CANCELLED)) { + warning("Could not register Dino's tray icon: %s", e.message); + } + } + + registration_in_progress = false; + if (!shutting_down && name_acquired && watcher_available && !registered_with_watcher && + watcher_owner != registering_watcher) { + register_with_watcher.begin(); + } + } + + private void show_window() { + if (application.window == null) { + application.activate(); + return; + } + + if (activation_token != null) { + application.window.set_startup_id(activation_token); + activation_token = null; + } + application.window.present(); + } +} + +} diff --git a/main/src/ui/status_notifier_menu.vala b/main/src/ui/status_notifier_menu.vala new file mode 100644 index 00000000..cc8b1375 --- /dev/null +++ b/main/src/ui/status_notifier_menu.vala @@ -0,0 +1,100 @@ +using GLib; + +namespace Dino.Ui { + +public struct DBusMenuLayout { + public int id; + public HashTable<string, Variant> properties; + public Variant[] children; +} + +[DBus (name = "com.canonical.dbusmenu")] +public class StatusNotifierMenu : Object { + private const int ROOT_ITEM_ID = 0; + private const int QUIT_ITEM_ID = 1; + + private weak Application application; + private bool quit_scheduled; + + public uint version { get { return 3; } } + public string text_direction { owned get { return "ltr"; } } + public string status { owned get { return "normal"; } } + public string[] icon_theme_path { owned get { return new string[0]; } } + + public signal void layout_updated(uint revision, int parent); + + public StatusNotifierMenu(Application application) { + this.application = application; + } + + public void get_layout(int parent_id, int recursion_depth, string[] property_names, + out uint revision, out DBusMenuLayout layout) throws Error { + revision = 1; + layout = create_layout(parent_id, recursion_depth, property_names); + } + + public void @event(int id, string event_id, Variant data, uint timestamp) throws Error { + if (id != QUIT_ITEM_ID || event_id != "clicked" || quit_scheduled) return; + + quit_scheduled = true; + Idle.add(() => { + application.quit(); + return Source.REMOVE; + }); + } + + public bool about_to_show(int id) throws Error { + return false; + } + + private DBusMenuLayout create_layout(int id, int recursion_depth, string[] property_names) { + DBusMenuLayout layout = DBusMenuLayout(); + layout.id = id; + layout.properties = get_item_properties(id, property_names); + layout.children = new Variant[0]; + + if (id == ROOT_ITEM_ID && recursion_depth != 0) { + DBusMenuLayout quit_item = create_layout(QUIT_ITEM_ID, 0, property_names); + layout.children = new Variant[] { layout_to_variant(quit_item) }; + } + return layout; + } + + private HashTable<string, Variant> get_item_properties(int id, string[] property_names) { + HashTable<string, Variant> properties = new HashTable<string, Variant>(str_hash, str_equal); + + if (id == ROOT_ITEM_ID) { + add_property(properties, property_names, "children-display", new Variant.string("submenu")); + } else if (id == QUIT_ITEM_ID) { + add_property(properties, property_names, "label", new Variant.string(_("Quit Dino"))); + } + return properties; + } + + private static void add_property(HashTable<string, Variant> properties, string[] property_names, + string name, Variant value) { + if (property_names.length == 0 || name in property_names) { + properties.insert(name, value); + } + } + + private static Variant layout_to_variant(DBusMenuLayout layout) { + VariantBuilder properties = new VariantBuilder(new VariantType("a{sv}")); + layout.properties.foreach((name, value) => { + properties.add("{sv}", name, value); + }); + + Variant[] children = new Variant[layout.children.length]; + for (int i = 0; i < layout.children.length; i++) { + children[i] = new Variant.variant(layout.children[i]); + } + + return new Variant.tuple(new Variant[] { + new Variant.int32(layout.id), + properties.end(), + new Variant.array(VariantType.VARIANT, children) + }); + } +} + +} |
