aboutsummaryrefslogtreecommitdiff
path: root/main/src
diff options
context:
space:
mode:
authorfrosty <gabriel@bwaaa.monster>2026-08-25 22:50:10 -0400
committerfrosty <gabriel@bwaaa.monster>2026-08-25 22:50:10 -0400
commitb19412a3d291f8cdbf037e00ac2f3641f7493de6 (patch)
tree8179bc8a64c2286fde58ce829f774ac3d0a32c1b /main/src
parentd2c69cfb544c23042652dee9ebf0e7031d6c2b29 (diff)
feat: add tray icon
Diffstat (limited to 'main/src')
-rw-r--r--main/src/ui/application.vala17
-rw-r--r--main/src/ui/status_notifier_item.vala206
-rw-r--r--main/src/ui/status_notifier_menu.vala100
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)
+ });
+ }
+}
+
+}