From 879f65f03b2417072aefb3d11a40797130d0e58a Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 29 Dec 2024 17:02:21 +0100 Subject: [PATCH 01/16] Prototype --- data/pantheon.portal | 2 +- src/Notification/Portal.vala | 70 +++++++++++ src/Notification/PortalNotification.vala | 144 +++++++++++++++++++++++ src/Notification/Widget.vala | 90 ++++++++++++++ src/Notification/Window.vala | 31 +++++ src/XdgDesktopPortalPantheon.vala | 3 + src/meson.build | 4 + 7 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 src/Notification/Portal.vala create mode 100644 src/Notification/PortalNotification.vala create mode 100644 src/Notification/Widget.vala create mode 100644 src/Notification/Window.vala diff --git a/data/pantheon.portal b/data/pantheon.portal index b9de94e3..b677a33e 100644 --- a/data/pantheon.portal +++ b/data/pantheon.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.pantheon -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Notification;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast UseIn=pantheon diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala new file mode 100644 index 00000000..60e1e4cc --- /dev/null +++ b/src/Notification/Portal.vala @@ -0,0 +1,70 @@ +// TEST CALL FOR DSPY: + +// ('io.elementary.mail.desktop', 'new-mail', {'title': <'New mail from John Doe'>, 'body': <'You have a new mail from John Doe. Click to read it.'>}) + + +[DBus (name = "org.freedesktop.impl.portal.Notification")] +public class Notification.Portal : Object { + public const string ID_FORMAT = "%s:%s"; + + public signal void action_invoked (string app_id, string id, string action_name, Variant[] parameters); + + public HashTable supported_options { get; construct; } + + [DBus (visible = false)] + public ListStore notifications { get; construct; } + + private HashTable notifications_by_id; + + private Gtk.Window? main_window; + + construct { + notifications = new ListStore (typeof (PortalNotification)); + notifications_by_id = new HashTable (str_hash, str_equal); + supported_options = new HashTable (str_hash, str_equal); + } + + public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { + var internal_id = ID_FORMAT.printf (app_id, id); + + var notification = new PortalNotification (app_id, id, data); + + if (internal_id in notifications_by_id) { + if (SHOW_AS_NEW in notification.display_hint) { + remove_notification (app_id, id); + } else { + notifications_by_id[internal_id].replace (data); + return; + } + } + + notification.dismissed.connect (remove_notification_internal); + notification.activate_action.connect ((name, target) => action_invoked (app_id, id, name, {target})); + notifications_by_id[internal_id] = notification; + notifications.append (notification); + + if (main_window == null) { + main_window = new MainWindow (this); + } + + Idle.add_once (() => main_window.present ()); + } + + public void remove_notification (string app_id, string id) throws DBusError, IOError { + var internal_id = ID_FORMAT.printf (app_id, id); + + remove_notification_internal (internal_id); + } + + private void remove_notification_internal (string internal_id) { + notifications_by_id.remove (internal_id); + + for (int i = 0; i < notifications.n_items; i++) { + var notification = (PortalNotification) notifications.get_object (i); + if (notification.id == internal_id) { + notifications.remove (i); + break; + } + } + } +} diff --git a/src/Notification/PortalNotification.vala b/src/Notification/PortalNotification.vala new file mode 100644 index 00000000..cd367eee --- /dev/null +++ b/src/Notification/PortalNotification.vala @@ -0,0 +1,144 @@ + +public class Notification.PortalNotification : GLib.Object { + public const string ACTION_GROUP_NAME = "action"; + public const string ACTION_PREFIX = ACTION_GROUP_NAME + "."; + + public class Button : Object { + public string label; + public string action_name; + public Variant? action_target = null; + } + + [Flags] + public enum DisplayHint { + TRANSIENT, + TRAY, + PERSISTENT, + HIDE_ON_LOCK_SCREEN, + HIDE_CONTENT_ON_LOCK_SCREEN, + SHOW_AS_NEW + } + + public signal void dismissed (string id); + public signal void activate_action (string name, Variant? target); + + public string id { get; construct; } + + public string app_id { get; construct; } + + /** + * The title of the notification, always uses markup. + */ + public string title { get; private set; } + + /** + * The body of the notification, always uses markup. + */ + public string body { get; private set; } + + public string time { get; private set; } // with 60 second timeout + + public Icon primary_icon { get; private set; } + public Icon? secondary_icon { get; private set; } + + public NotificationPriority priority { get; private set; default = NORMAL; } + + public SimpleActionGroup actions { get; private set; } + public ListStore buttons { get; private set; } + + public DisplayHint display_hint { get; private set; } + + public PortalNotification (string app_id, string id, HashTable data) { + Object (app_id: app_id, id: Portal.ID_FORMAT.printf (app_id, id)); + + replace (data); + } + + construct { + buttons = new ListStore (typeof (Button)); + actions = new SimpleActionGroup (); + } + + public void replace (HashTable data) { + buttons.remove_all (); + + if ("title" in data) { + title = data["title"].get_string (); + } + + if ("body" in data) { + body = data["body"].get_string (); + } + + if ("markup-body" in data) { + body = data["markup-body"].get_string (); + } + + var desktop_app_info = new DesktopAppInfo (app_id + ".desktop"); + + primary_icon = desktop_app_info.get_icon (); + + if ("icon" in data) { + // do some shit + } + + if ("priority" in data) { + var priority = data["priority"].get_string (); + + switch (priority) { + case "low": + this.priority = LOW; + break; + + case "normal": + default: + this.priority = NORMAL; + break; + } + } + + if ("default-action" in data) { + var default_action = new SimpleAction ("default", null); + actions.add_action (default_action); + + if ("default-action-target" in data) { + default_action.activate.connect (() => activate_action (data["default-action"].get_string (), data["default-action-target"])); + } else { + default_action.activate.connect (() => activate_action (data["default-action"].get_string (), null)); + } + } + + if ("buttons" in data) { + var buttons = (HashTable[]) data["buttons"]; + + foreach (var button_data in buttons) { + var button = new Button (); + + if ("label" in button_data) { + button.label = button_data["label"].get_string (); + } + + if ("action" in button_data) { + button.action_name = button_data["action"].get_string (); + } + + if ("action-target" in button_data) { + button.action_target = button_data["action-target"]; + } + + this.buttons.append (button); + var action = new SimpleAction (button.action_name, button.action_target != null ? button.action_target.get_type () : null); + actions.add_action (action); + action.activate.connect ((target) => activate_action (button.action_name, target)); + } + } + } + + public void play_sound () { + + } + + public void dismiss () { + dismissed (id); + } +} diff --git a/src/Notification/Widget.vala b/src/Notification/Widget.vala new file mode 100644 index 00000000..c2e20937 --- /dev/null +++ b/src/Notification/Widget.vala @@ -0,0 +1,90 @@ +public class Notification.Widget : Granite.Bin { + public PortalNotification notification { get; construct; } + + public Gtk.Label time_label { get; construct; } + + private Gtk.FlowBox button_box; + + public Widget (PortalNotification notification) { + Object (notification: notification); + } + + construct { + var primary_icon = new Gtk.Image (); + notification.bind_property ("primary-icon", primary_icon, "gicon", SYNC_CREATE); + + var secondary_icon = new Gtk.Image (); + bind_with_visible ("secondary-icon", secondary_icon, "gicon"); + + var icon_overlay = new Gtk.Overlay () { + child = primary_icon + }; + icon_overlay.add_overlay (secondary_icon); + + var title_label = new Gtk.Label (null) { + halign = START, + margin_start = 12, + margin_end = 12, + margin_top = 12, + margin_bottom = 6, + wrap = true, + max_width_chars = 50, + ellipsize = END, + use_markup = true + }; + notification.bind_property ("title", title_label, "label", SYNC_CREATE); + + time_label = new Gtk.Label (null); + notification.bind_property ("time", time_label, "label", SYNC_CREATE); + + var body_label = new Gtk.Label (null) { + halign = START, + margin_start = 12, + margin_end = 12, + margin_top = 12, + margin_bottom = 6, + wrap = true, + max_width_chars = 50, + ellipsize = END, + use_markup = true + }; + notification.bind_property ("body", body_label, "label", SYNC_CREATE); + + button_box = new Gtk.FlowBox () { + halign = END, + margin_start = 12, + margin_end = 12, + margin_top = 6, + margin_bottom = 6, + selection_mode = NONE, + homogeneous = true + }; + button_box.bind_model (notification.buttons, create_button_func); + + var grid = new Gtk.Grid (); + grid.attach (icon_overlay, 0, 0, 1, 2); + grid.attach (title_label, 1, 0, 1, 1); + grid.attach (time_label, 2, 0, 1, 1); + grid.attach (body_label, 1, 1, 2, 1); + grid.attach (button_box, 1, 2, 2, 1); + + child = grid; + insert_action_group (PortalNotification.ACTION_GROUP_NAME, notification.actions); + + var gesture_click = new Gtk.GestureClick (); + gesture_click.pressed.connect (() => activate_action ("default", null)); + add_controller (gesture_click); + } + + private Gtk.Widget create_button_func (Object obj) { + var button = (PortalNotification.Button) obj; + return new Gtk.Button.with_label (button.label) { + action_name = PortalNotification.ACTION_PREFIX + button.action_name, + action_target = button.action_target + }; + } + + private void bind_with_visible (string property, Gtk.Widget widget, string widget_property) { + notification.bind_property (property, widget, widget_property, SYNC_CREATE); + } +} diff --git a/src/Notification/Window.vala b/src/Notification/Window.vala new file mode 100644 index 00000000..cf44ab22 --- /dev/null +++ b/src/Notification/Window.vala @@ -0,0 +1,31 @@ +/* +* SPDX-License-Identifier: GPL-3.0-or-later +* SPDX-FileCopyrightText: {{YEAR}} {{DEVELOPER_NAME}} <{{DEVELOPER_EMAIL}}> +*/ + +public class Notification.MainWindow : Gtk.Window { + public Portal portal { get; construct; } + + public MainWindow (Portal portal) { + Object ( + default_height: 500, + default_width: 500, + title: _("My App Name"), + portal: portal + ); + } + + construct { + var list_box = new Gtk.ListBox (); + list_box.bind_model (portal.notifications, create_widget_func); + + child = list_box; + titlebar = new Gtk.Grid () { visible = false }; + } + + private Gtk.Widget create_widget_func (Object obj) { + var notification = (PortalNotification) obj; + var widget = new Widget (notification); + return widget; + } +} diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index 56056120..b50bac6a 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -41,6 +41,9 @@ private void on_bus_acquired (DBusConnection connection, string name) { connection.register_object ("/org/freedesktop/portal/desktop", new Background.Portal (connection)); debug ("Background Portal registered!"); + connection.register_object ("/org/freedesktop/portal/desktop", new Notification.Portal ()); + debug ("Notification Portal registered!"); + connection.register_object ("/org/freedesktop/portal/desktop", new Screenshot.Portal (connection)); debug ("Screenshot Portal registered!"); diff --git a/src/meson.build b/src/meson.build index f995b995..09ee0279 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,6 +8,10 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', + 'Notification/Portal.vala', + 'Notification/PortalNotification.vala', + 'Notification/Widget.vala', + 'Notification/Window.vala', 'ScreenCast/MonitorTracker/Interface.vala', 'ScreenCast/MonitorTracker/Monitor.vala', 'ScreenCast/MonitorTracker/MonitorTracker.vala', From d00ed8c4f96cc14ffac161c5916fe8ff2ec312bd Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 3 Jan 2025 17:51:20 +0100 Subject: [PATCH 02/16] Iterate --- src/Notification/Portal.vala | 106 ++++++++++++++++------- src/Notification/PortalNotification.vala | 69 +++++++++------ src/Notification/Widget.vala | 10 +-- src/Notification/Window.vala | 2 +- 4 files changed, 123 insertions(+), 64 deletions(-) diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 60e1e4cc..ab06b52a 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -2,10 +2,16 @@ // ('io.elementary.mail.desktop', 'new-mail', {'title': <'New mail from John Doe'>, 'body': <'You have a new mail from John Doe. Click to read it.'>}) - +/** + * The portal. + * On receiving: + * - check if already there + */ [DBus (name = "org.freedesktop.impl.portal.Notification")] public class Notification.Portal : Object { public const string ID_FORMAT = "%s:%s"; + public const string ACTION_FORMAT = "%s+action+%s"; // interal id, action id + public const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action id public signal void action_invoked (string app_id, string id, string action_name, Variant[] parameters); @@ -13,57 +19,91 @@ public class Notification.Portal : Object { [DBus (visible = false)] public ListStore notifications { get; construct; } - - private HashTable notifications_by_id; - - private Gtk.Window? main_window; + [DBus (visible = false)] + public SimpleActionGroup actions { get; construct; } construct { - notifications = new ListStore (typeof (PortalNotification)); - notifications_by_id = new HashTable (str_hash, str_equal); supported_options = new HashTable (str_hash, str_equal); + + notifications = new ListStore (typeof (Notification)); + actions = new SimpleActionGroup (); } public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { var internal_id = ID_FORMAT.printf (app_id, id); + var notification = new Notification (app_id, id, data, this); - var notification = new PortalNotification (app_id, id, data); + replace_notification_internal (internal_id, notification); + } - if (internal_id in notifications_by_id) { - if (SHOW_AS_NEW in notification.display_hint) { - remove_notification (app_id, id); - } else { - notifications_by_id[internal_id].replace (data); - return; + public void remove_notification (string app_id, string id) throws DBusError, IOError { + var internal_id = ID_FORMAT.printf (app_id, id); + + replace_notification_internal (internal_id, null); + } + + /** + * Removes the given id and if not null replaces it with the given notification at the same position. + * If SHOW_AS_NEW is set in the display hint of the replacement, it will be added at the front instead of at the same position. + * If no notification with the given id is found, and the replacement is not null, the replacement will be added at the front. + */ + private void replace_notification_internal (string internal_id, Notification? replacement) { + for (int i = 0; i < notifications.n_items; i++) { + var notification = (Notification) notifications.get_object (i); + if (notification.id == internal_id) { + if (replacement == null) { // Just remove and return + notifications.remove (i); + return; + } else if (SHOW_AS_NEW in replacement.display_hint) { // Remove but don't return because we want to add the replacement as if it was a new notification + notifications.remove (i); + } else { // Replace and return + notifications.splice (i, 1, { replacement }); + return; + } } } - notification.dismissed.connect (remove_notification_internal); - notification.activate_action.connect ((name, target) => action_invoked (app_id, id, name, {target})); - notifications_by_id[internal_id] = notification; - notifications.append (notification); + notifications.append (replacement); + } + + internal void on_action (SimpleAction action, Variant? parameter) { + var name = action.name; + + var parts = name.split ("+", 3); - if (main_window == null) { - main_window = new MainWindow (this); + if (parts.length != 3) { + warning ("Invalid action name: %s", name); + return; } - Idle.add_once (() => main_window.present ()); - } + var internal_id = parts[0]; + var type = parts[1]; + var action_name = parts[2]; - public void remove_notification (string app_id, string id) throws DBusError, IOError { - var internal_id = ID_FORMAT.printf (app_id, id); + var id_parts = internal_id.split (":", 2); - remove_notification_internal (internal_id); - } + if (id_parts.length != 2) { + warning ("Invalid internal id: %s", internal_id); + return; + } - private void remove_notification_internal (string internal_id) { - notifications_by_id.remove (internal_id); + var app_id = id_parts[0]; + var notification_id = id_parts[1]; - for (int i = 0; i < notifications.n_items; i++) { - var notification = (PortalNotification) notifications.get_object (i); - if (notification.id == internal_id) { - notifications.remove (i); - break; + if (type == "action") { + action_invoked (app_id, notification_id, action_name, { parameter }); + } else { + switch (action_name) { + case "default": + // launch + break; + + case "dismiss": + replace_notification_internal (internal_id, null); + break; + + default: + break; } } } diff --git a/src/Notification/PortalNotification.vala b/src/Notification/PortalNotification.vala index cd367eee..00e96a94 100644 --- a/src/Notification/PortalNotification.vala +++ b/src/Notification/PortalNotification.vala @@ -1,5 +1,5 @@ -public class Notification.PortalNotification : GLib.Object { +public class Notification.Notification : GLib.Object { public const string ACTION_GROUP_NAME = "action"; public const string ACTION_PREFIX = ACTION_GROUP_NAME + "."; @@ -19,13 +19,14 @@ public class Notification.PortalNotification : GLib.Object { SHOW_AS_NEW } - public signal void dismissed (string id); - public signal void activate_action (string name, Variant? target); + public unowned Portal portal { get; construct; } public string id { get; construct; } public string app_id { get; construct; } + public HashTable data { get; construct; } + /** * The title of the notification, always uses markup. */ @@ -43,24 +44,25 @@ public class Notification.PortalNotification : GLib.Object { public NotificationPriority priority { get; private set; default = NORMAL; } - public SimpleActionGroup actions { get; private set; } + public string default_action_name { get; construct; } + public Variant? default_action_target { get; construct; } + public ListStore buttons { get; private set; } public DisplayHint display_hint { get; private set; } - public PortalNotification (string app_id, string id, HashTable data) { - Object (app_id: app_id, id: Portal.ID_FORMAT.printf (app_id, id)); + private ActionEntry[] action_entries; - replace (data); + public Notification (string app_id, string id, HashTable data, Portal portal) { + Object (app_id: app_id, id: Portal.ID_FORMAT.printf (app_id, id), data: data, portal: portal); } - construct { - buttons = new ListStore (typeof (Button)); - actions = new SimpleActionGroup (); + ~Notification () { + portal.actions.remove_action_entries (action_entries); } - public void replace (HashTable data) { - buttons.remove_all (); + construct { + buttons = new ListStore (typeof (Button)); if ("title" in data) { title = data["title"].get_string (); @@ -97,15 +99,31 @@ public class Notification.PortalNotification : GLib.Object { } } + ActionEntry default_action_entry; + if ("default-action" in data) { - var default_action = new SimpleAction ("default", null); - actions.add_action (default_action); + var default_action_name = Portal.ACTION_FORMAT.printf (id, data["default-action"].get_string ()); + string? default_action_parameter = null; if ("default-action-target" in data) { - default_action.activate.connect (() => activate_action (data["default-action"].get_string (), data["default-action-target"])); - } else { - default_action.activate.connect (() => activate_action (data["default-action"].get_string (), null)); + default_action_parameter = default_action_entry.parameter_type = data["default-action-target"].get_type_string (); } + + default_action_entry = ActionEntry () { + name = default_action_name, + activate = portal.on_action, + parameter_type = default_action_parameter + }; + + this.default_action_name = default_action_name; + this.default_action_target = data["default-action-target"]; + } else { + default_action_entry = ActionEntry () { + name = Portal.INTERNAL_ACTION_FORMAT.printf (id, "default"), + activate = portal.on_action + }; + + default_action_name = default_action_entry.name; } if ("buttons" in data) { @@ -119,7 +137,7 @@ public class Notification.PortalNotification : GLib.Object { } if ("action" in button_data) { - button.action_name = button_data["action"].get_string (); + button.action_name = Portal.ACTION_FORMAT.printf (id, button_data["action"].get_string ()); } if ("action-target" in button_data) { @@ -127,18 +145,19 @@ public class Notification.PortalNotification : GLib.Object { } this.buttons.append (button); - var action = new SimpleAction (button.action_name, button.action_target != null ? button.action_target.get_type () : null); - actions.add_action (action); - action.activate.connect ((target) => activate_action (button.action_name, target)); + + action_entries += ActionEntry () { + name = button.action_name, + activate = portal.on_action, + parameter_type = button.action_target != null ? button.action_target.get_type ().dup_string () : null + }; } } + + portal.actions.add_action_entries (action_entries, portal); } public void play_sound () { } - - public void dismiss () { - dismissed (id); - } } diff --git a/src/Notification/Widget.vala b/src/Notification/Widget.vala index c2e20937..b8c439aa 100644 --- a/src/Notification/Widget.vala +++ b/src/Notification/Widget.vala @@ -1,11 +1,11 @@ public class Notification.Widget : Granite.Bin { - public PortalNotification notification { get; construct; } + public Notification notification { get; construct; } public Gtk.Label time_label { get; construct; } private Gtk.FlowBox button_box; - public Widget (PortalNotification notification) { + public Widget (Notification notification) { Object (notification: notification); } @@ -69,7 +69,7 @@ public class Notification.Widget : Granite.Bin { grid.attach (button_box, 1, 2, 2, 1); child = grid; - insert_action_group (PortalNotification.ACTION_GROUP_NAME, notification.actions); + insert_action_group (Notification.ACTION_GROUP_NAME, notification.portal.actions); var gesture_click = new Gtk.GestureClick (); gesture_click.pressed.connect (() => activate_action ("default", null)); @@ -77,9 +77,9 @@ public class Notification.Widget : Granite.Bin { } private Gtk.Widget create_button_func (Object obj) { - var button = (PortalNotification.Button) obj; + var button = (Notification.Button) obj; return new Gtk.Button.with_label (button.label) { - action_name = PortalNotification.ACTION_PREFIX + button.action_name, + action_name = Notification.ACTION_PREFIX + button.action_name, action_target = button.action_target }; } diff --git a/src/Notification/Window.vala b/src/Notification/Window.vala index cf44ab22..9fa6f24a 100644 --- a/src/Notification/Window.vala +++ b/src/Notification/Window.vala @@ -24,7 +24,7 @@ public class Notification.MainWindow : Gtk.Window { } private Gtk.Widget create_widget_func (Object obj) { - var notification = (PortalNotification) obj; + var notification = (Notification) obj; var widget = new Widget (notification); return widget; } From 894ece30875b06ceaebba2f9406e61fe9aade59b Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 3 Jan 2025 18:02:32 +0100 Subject: [PATCH 03/16] Add create_action_entry --- src/Notification/Portal.vala | 6 +++++- src/Notification/PortalNotification.vala | 18 +++--------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index ab06b52a..893ca0e2 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -66,7 +66,11 @@ public class Notification.Portal : Object { notifications.append (replacement); } - internal void on_action (SimpleAction action, Variant? parameter) { + internal ActionEntry create_action_entry (string name, string? target_type) { + return { name, on_action, target_type }; + } + + private void on_action (SimpleAction action, Variant? parameter) { var name = action.name; var parts = name.split ("+", 3); diff --git a/src/Notification/PortalNotification.vala b/src/Notification/PortalNotification.vala index 00e96a94..de9e84d5 100644 --- a/src/Notification/PortalNotification.vala +++ b/src/Notification/PortalNotification.vala @@ -109,20 +109,12 @@ public class Notification.Notification : GLib.Object { default_action_parameter = default_action_entry.parameter_type = data["default-action-target"].get_type_string (); } - default_action_entry = ActionEntry () { - name = default_action_name, - activate = portal.on_action, - parameter_type = default_action_parameter - }; + default_action_entry = portal.create_action_entry (default_action_name, default_action_parameter); this.default_action_name = default_action_name; this.default_action_target = data["default-action-target"]; } else { - default_action_entry = ActionEntry () { - name = Portal.INTERNAL_ACTION_FORMAT.printf (id, "default"), - activate = portal.on_action - }; - + default_action_entry = portal.create_action_entry (Portal.INTERNAL_ACTION_FORMAT.printf (id, "default"), null); default_action_name = default_action_entry.name; } @@ -146,11 +138,7 @@ public class Notification.Notification : GLib.Object { this.buttons.append (button); - action_entries += ActionEntry () { - name = button.action_name, - activate = portal.on_action, - parameter_type = button.action_target != null ? button.action_target.get_type ().dup_string () : null - }; + action_entries += portal.create_action_entry (button.action_name, button.action_target != null ? button.action_target.get_type_string () : null); } } From c54bcda3e8230a956bf9b916e8e8cdb7b76d6a87 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 3 Jan 2025 18:46:10 +0100 Subject: [PATCH 04/16] Iterate more --- src/Notification/ActionGroup.vala | 131 ++++++++++++++++++ src/Notification/DBus.vala | 25 ++++ ...talNotification.vala => Notification.vala} | 33 +---- src/Notification/Portal.vala | 58 +------- src/meson.build | 4 +- 5 files changed, 172 insertions(+), 79 deletions(-) create mode 100644 src/Notification/ActionGroup.vala create mode 100644 src/Notification/DBus.vala rename src/Notification/{PortalNotification.vala => Notification.vala} (72%) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala new file mode 100644 index 00000000..c2b51234 --- /dev/null +++ b/src/Notification/ActionGroup.vala @@ -0,0 +1,131 @@ +public class Notification.ActionGroup : Object, GLib.ActionGroup { + public Portal portal { get; construct; } + + public ActionGroup (Portal portal) { + Object (portal: portal); + } + + public string[] list_actions () { + var builder = new StrvBuilder (); + + for (uint i = 0; i < portal.notifications.n_items; i++) { + var notification = (Notification) portal.notifications.get_item (i); + + builder.add (notification.default_action_name); + builder.add (notification.close_action_name); + + for (uint j = 0; j < notification.buttons.n_items; j++) { + var button = (Notification.Button) notification.buttons.get_item (j); + builder.add (button.action_name); + } + } + + return builder.end (); + } + + public void activate_action (string name, Variant? target) { + var parts = name.split ("+", 3); + + if (parts.length != 3) { + warning ("Invalid action name: %s", name); + return; + } + + var internal_id = parts[0]; + var type = parts[1]; + var action_name = parts[2]; + + var id_parts = internal_id.split (":", 2); + + if (id_parts.length != 2) { + warning ("Invalid internal id: %s", internal_id); + return; + } + + var app_id = id_parts[0]; + var notification_id = id_parts[1]; + + if (type == "action") { + portal.action_invoked (app_id, notification_id, action_name, { target }); + } else { + switch (action_name) { + case "default": + // launch + break; + + case "dismiss": + portal.replace_notification (internal_id, null); + break; + + default: + break; + } + } + } + + public override bool query_action ( + string name, + out bool enabled, + out unowned VariantType parameter_type, + out unowned VariantType state_type, + out Variant state_hint, + out Variant state + ) { + enabled = true; + state_type = null; + state_hint = null; + state = null; + + var parts = name.split ("+", 3); + + if (parts.length != 3) { + warning ("Invalid action name: %s", name); + return false; + } + + var internal_id = parts[0]; + var type = parts[1]; + var action_name = parts[2]; + + Notification? notification; + for (uint i = 0; i < portal.notifications.n_items; i++) { + var n = (Notification) portal.notifications.get_item (i); + if (n.id == internal_id) { + notification = n; + break; + } + } + + if (notification == null) { + warning ("Notification not found: %s", internal_id); + return false; + } + + if (type == "action") { + for (uint i = 0; i < notification.buttons.n_items; i++) { + var button = (Notification.Button) notification.buttons.get_item (i); + if (button.action_name == action_name) { + parameter_type = button.action_target.get_type (); + return true; + } + } + } else { + switch (action_name) { + case "default": + parameter_type = notification.default_action_target != null ? notification.default_action_target.get_type () : null; + return true; + + case "dismiss": + parameter_type = null; + return true; + + default: + return false; + } + } + + return true; + } + + public void change_action_state (string action_name, Variant value) { } +} diff --git a/src/Notification/DBus.vala b/src/Notification/DBus.vala new file mode 100644 index 00000000..c03f5817 --- /dev/null +++ b/src/Notification/DBus.vala @@ -0,0 +1,25 @@ +[DBus (name = "io.elementary.portal.NotificationsProvider")] +public class Notification.Provider : Object { + public signal void items_changed (uint pos, uint removed, HashTable[] added); + + [DBus (visible = false)] + public Portal portal { get; construct; } + + public Provider (Portal portal) { + Object (portal: portal); + } + + construct { + portal.notifications.items_changed.connect (on_items_changed); + } + + private void on_items_changed (uint pos, uint removed, uint added) { + HashTable[] added_notifications = new HashTable[added]; + + for (uint i = 0; i < added; i++) { + added_notifications[i] = ((Notification) portal.notifications.get_item (pos + i)).data; + } + + items_changed (pos, removed, added_notifications); + } +} diff --git a/src/Notification/PortalNotification.vala b/src/Notification/Notification.vala similarity index 72% rename from src/Notification/PortalNotification.vala rename to src/Notification/Notification.vala index de9e84d5..d8f356cb 100644 --- a/src/Notification/PortalNotification.vala +++ b/src/Notification/Notification.vala @@ -19,8 +19,6 @@ public class Notification.Notification : GLib.Object { SHOW_AS_NEW } - public unowned Portal portal { get; construct; } - public string id { get; construct; } public string app_id { get; construct; } @@ -44,6 +42,8 @@ public class Notification.Notification : GLib.Object { public NotificationPriority priority { get; private set; default = NORMAL; } + public string close_action_name { owned get { return Portal.INTERNAL_ACTION_FORMAT.printf (id, "dismiss"); } } + public string default_action_name { get; construct; } public Variant? default_action_target { get; construct; } @@ -51,14 +51,8 @@ public class Notification.Notification : GLib.Object { public DisplayHint display_hint { get; private set; } - private ActionEntry[] action_entries; - - public Notification (string app_id, string id, HashTable data, Portal portal) { - Object (app_id: app_id, id: Portal.ID_FORMAT.printf (app_id, id), data: data, portal: portal); - } - - ~Notification () { - portal.actions.remove_action_entries (action_entries); + public Notification (string app_id, string id, HashTable data) { + Object (app_id: app_id, id: Portal.ID_FORMAT.printf (app_id, id), data: data); } construct { @@ -99,23 +93,14 @@ public class Notification.Notification : GLib.Object { } } - ActionEntry default_action_entry; - if ("default-action" in data) { - var default_action_name = Portal.ACTION_FORMAT.printf (id, data["default-action"].get_string ()); + default_action_name = Portal.ACTION_FORMAT.printf (id, data["default-action"].get_string ()); - string? default_action_parameter = null; if ("default-action-target" in data) { - default_action_parameter = default_action_entry.parameter_type = data["default-action-target"].get_type_string (); + default_action_target = data["default-action-target"]; } - - default_action_entry = portal.create_action_entry (default_action_name, default_action_parameter); - - this.default_action_name = default_action_name; - this.default_action_target = data["default-action-target"]; } else { - default_action_entry = portal.create_action_entry (Portal.INTERNAL_ACTION_FORMAT.printf (id, "default"), null); - default_action_name = default_action_entry.name; + default_action_name = Portal.INTERNAL_ACTION_FORMAT.printf (id, "default"); } if ("buttons" in data) { @@ -137,12 +122,8 @@ public class Notification.Notification : GLib.Object { } this.buttons.append (button); - - action_entries += portal.create_action_entry (button.action_name, button.action_target != null ? button.action_target.get_type_string () : null); } } - - portal.actions.add_action_entries (action_entries, portal); } public void play_sound () { diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 893ca0e2..e443276b 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -20,26 +20,26 @@ public class Notification.Portal : Object { [DBus (visible = false)] public ListStore notifications { get; construct; } [DBus (visible = false)] - public SimpleActionGroup actions { get; construct; } + public ActionGroup actions { get; construct; } construct { supported_options = new HashTable (str_hash, str_equal); notifications = new ListStore (typeof (Notification)); - actions = new SimpleActionGroup (); + actions = new ActionGroup (this); } public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { var internal_id = ID_FORMAT.printf (app_id, id); - var notification = new Notification (app_id, id, data, this); + var notification = new Notification (app_id, id, data); - replace_notification_internal (internal_id, notification); + replace_notification (internal_id, notification); } public void remove_notification (string app_id, string id) throws DBusError, IOError { var internal_id = ID_FORMAT.printf (app_id, id); - replace_notification_internal (internal_id, null); + replace_notification (internal_id, null); } /** @@ -47,7 +47,7 @@ public class Notification.Portal : Object { * If SHOW_AS_NEW is set in the display hint of the replacement, it will be added at the front instead of at the same position. * If no notification with the given id is found, and the replacement is not null, the replacement will be added at the front. */ - private void replace_notification_internal (string internal_id, Notification? replacement) { + internal void replace_notification (string internal_id, Notification? replacement) { for (int i = 0; i < notifications.n_items; i++) { var notification = (Notification) notifications.get_object (i); if (notification.id == internal_id) { @@ -65,50 +65,4 @@ public class Notification.Portal : Object { notifications.append (replacement); } - - internal ActionEntry create_action_entry (string name, string? target_type) { - return { name, on_action, target_type }; - } - - private void on_action (SimpleAction action, Variant? parameter) { - var name = action.name; - - var parts = name.split ("+", 3); - - if (parts.length != 3) { - warning ("Invalid action name: %s", name); - return; - } - - var internal_id = parts[0]; - var type = parts[1]; - var action_name = parts[2]; - - var id_parts = internal_id.split (":", 2); - - if (id_parts.length != 2) { - warning ("Invalid internal id: %s", internal_id); - return; - } - - var app_id = id_parts[0]; - var notification_id = id_parts[1]; - - if (type == "action") { - action_invoked (app_id, notification_id, action_name, { parameter }); - } else { - switch (action_name) { - case "default": - // launch - break; - - case "dismiss": - replace_notification_internal (internal_id, null); - break; - - default: - break; - } - } - } } diff --git a/src/meson.build b/src/meson.build index 09ee0279..c94da5a0 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,8 +8,10 @@ executable( 'AppChooser/Portal.vala', 'Background/NotificationRequest.vala', 'Background/Portal.vala', + 'Notification/ActionGroup.vala', + 'Notification/DBus.vala', + 'Notification/Notification.vala', 'Notification/Portal.vala', - 'Notification/PortalNotification.vala', 'Notification/Widget.vala', 'Notification/Window.vala', 'ScreenCast/MonitorTracker/Interface.vala', From 271f9ded43b802717ebbd4500f0c2ec7f95d1f7d Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 3 Jan 2025 18:52:53 +0100 Subject: [PATCH 05/16] Refinements --- src/Notification/ActionGroup.vala | 9 ++++++++- src/Notification/Notification.vala | 10 ++++++---- src/Notification/Portal.vala | 2 -- src/Notification/Widget.vala | 3 +-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index c2b51234..ca1bdc92 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -1,3 +1,9 @@ + +/** + * Handles all action logic for notifications. It automatically tracks currently available notifications + * and lists their available actions. On activation it will emit the action_invoked signal on the portal, or + * close the notification or launch the application. + */ public class Notification.ActionGroup : Object, GLib.ActionGroup { public Portal portal { get; construct; } @@ -72,6 +78,7 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { out Variant state ) { enabled = true; + parameter_type = null; state_type = null; state_hint = null; state = null; @@ -87,7 +94,7 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { var type = parts[1]; var action_name = parts[2]; - Notification? notification; + Notification? notification = null; for (uint i = 0; i < portal.notifications.n_items; i++) { var n = (Notification) portal.notifications.get_item (i); if (n.id == internal_id) { diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index d8f356cb..6e973de9 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -2,6 +2,8 @@ public class Notification.Notification : GLib.Object { public const string ACTION_GROUP_NAME = "action"; public const string ACTION_PREFIX = ACTION_GROUP_NAME + "."; + public const string ACTION_FORMAT = "%s+action+%s"; // interal id, action id + public const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action id public class Button : Object { public string label; @@ -42,7 +44,7 @@ public class Notification.Notification : GLib.Object { public NotificationPriority priority { get; private set; default = NORMAL; } - public string close_action_name { owned get { return Portal.INTERNAL_ACTION_FORMAT.printf (id, "dismiss"); } } + public string close_action_name { owned get { return INTERNAL_ACTION_FORMAT.printf (id, "dismiss"); } } public string default_action_name { get; construct; } public Variant? default_action_target { get; construct; } @@ -94,13 +96,13 @@ public class Notification.Notification : GLib.Object { } if ("default-action" in data) { - default_action_name = Portal.ACTION_FORMAT.printf (id, data["default-action"].get_string ()); + default_action_name = ACTION_FORMAT.printf (id, data["default-action"].get_string ()); if ("default-action-target" in data) { default_action_target = data["default-action-target"]; } } else { - default_action_name = Portal.INTERNAL_ACTION_FORMAT.printf (id, "default"); + default_action_name = INTERNAL_ACTION_FORMAT.printf (id, "default"); } if ("buttons" in data) { @@ -114,7 +116,7 @@ public class Notification.Notification : GLib.Object { } if ("action" in button_data) { - button.action_name = Portal.ACTION_FORMAT.printf (id, button_data["action"].get_string ()); + button.action_name = ACTION_FORMAT.printf (id, button_data["action"].get_string ()); } if ("action-target" in button_data) { diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index e443276b..eab30c25 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -10,8 +10,6 @@ [DBus (name = "org.freedesktop.impl.portal.Notification")] public class Notification.Portal : Object { public const string ID_FORMAT = "%s:%s"; - public const string ACTION_FORMAT = "%s+action+%s"; // interal id, action id - public const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action id public signal void action_invoked (string app_id, string id, string action_name, Variant[] parameters); diff --git a/src/Notification/Widget.vala b/src/Notification/Widget.vala index b8c439aa..70e954d8 100644 --- a/src/Notification/Widget.vala +++ b/src/Notification/Widget.vala @@ -69,10 +69,9 @@ public class Notification.Widget : Granite.Bin { grid.attach (button_box, 1, 2, 2, 1); child = grid; - insert_action_group (Notification.ACTION_GROUP_NAME, notification.portal.actions); var gesture_click = new Gtk.GestureClick (); - gesture_click.pressed.connect (() => activate_action ("default", null)); + gesture_click.pressed.connect (() => activate_action_variant (notification.default_action_name, notification.default_action_target)); add_controller (gesture_click); } From 44e0defcbee52368759e415675f4241bee5f7fea Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 4 Jan 2025 00:45:10 +0100 Subject: [PATCH 06/16] BubbleManager, etc. --- src/Notification/BubbleManager.vala | 24 +++++++++++++++++ src/Notification/DBus.vala | 6 ++--- src/Notification/Portal.vala | 18 ++++++++++--- src/Notification/Widgets/Bubble.vala | 14 ++++++++++ src/Notification/{ => Widgets}/Widget.vala | 0 src/Notification/Window.vala | 31 ---------------------- src/meson.build | 5 ++-- 7 files changed, 58 insertions(+), 40 deletions(-) create mode 100644 src/Notification/BubbleManager.vala create mode 100644 src/Notification/Widgets/Bubble.vala rename src/Notification/{ => Widgets}/Widget.vala (100%) delete mode 100644 src/Notification/Window.vala diff --git a/src/Notification/BubbleManager.vala b/src/Notification/BubbleManager.vala new file mode 100644 index 00000000..53222ee5 --- /dev/null +++ b/src/Notification/BubbleManager.vala @@ -0,0 +1,24 @@ + + +public class Notification.BubbleManager : Object { + public Portal portal { get; construct; } + + public BubbleManager (Portal portal) { + Object (portal: portal); + } + + construct { + portal.notifications.items_changed.connect (on_items_changed); + } + + private void on_items_changed (uint pos, uint removed, uint added) { + if (pos != 0 || removed != 0) { + return; + } + + var added_notification = (Notification) portal.notifications.get_item (pos); + + var bubble = new Bubble (portal, added_notification); + bubble.present (); + } +} diff --git a/src/Notification/DBus.vala b/src/Notification/DBus.vala index c03f5817..5526bf8e 100644 --- a/src/Notification/DBus.vala +++ b/src/Notification/DBus.vala @@ -1,11 +1,11 @@ -[DBus (name = "io.elementary.portal.NotificationsProvider")] -public class Notification.Provider : Object { +[DBus (name = "io.elementary.portal.NotificationProvider")] +public class Notification.DBusProvider : Object { public signal void items_changed (uint pos, uint removed, HashTable[] added); [DBus (visible = false)] public Portal portal { get; construct; } - public Provider (Portal portal) { + public DBusProvider (Portal portal) { Object (portal: portal); } diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index eab30c25..1d17fef0 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -3,9 +3,13 @@ // ('io.elementary.mail.desktop', 'new-mail', {'title': <'New mail from John Doe'>, 'body': <'You have a new mail from John Doe. Click to read it.'>}) /** - * The portal. - * On receiving: - * - check if already there + * The notififcations portal consists of a few parts. Most importantly this class which exposes the portal + * api, tracks currently active notifications and holds the other parts. + * The {@link ActionGroup} handles all action logic for notifications. It automatically exposes all actions + * for all available notifications and handles the activation of these actions (by talking to #this). + * The {@link BubbleManager} is responsible for showing the notifications to the user in a bubble. + * The {@link DBusProvider} is responsible for exposing the notifications to the DBus for consumption by the indicator. It also exports the {@link actions}. + * Both {@link BubbleManager} and {@link DBusProvider} use the {@link actions} for all interaction (dismissing, activating actions). */ [DBus (name = "org.freedesktop.impl.portal.Notification")] public class Notification.Portal : Object { @@ -20,11 +24,17 @@ public class Notification.Portal : Object { [DBus (visible = false)] public ActionGroup actions { get; construct; } + private DBusProvider dbus_provider; + private BubbleManager bubble_manager; + construct { supported_options = new HashTable (str_hash, str_equal); notifications = new ListStore (typeof (Notification)); actions = new ActionGroup (this); + + dbus_provider = new DBusProvider (this); + bubble_manager = new BubbleManager (this); } public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { @@ -61,6 +71,6 @@ public class Notification.Portal : Object { } } - notifications.append (replacement); + notifications.splice (0, 0, { replacement }); } } diff --git a/src/Notification/Widgets/Bubble.vala b/src/Notification/Widgets/Bubble.vala new file mode 100644 index 00000000..41a23454 --- /dev/null +++ b/src/Notification/Widgets/Bubble.vala @@ -0,0 +1,14 @@ +public class Notification.Bubble : Gtk.Window { + public Portal portal { get; construct; } + public Notification notification { get; construct; } + + public Bubble (Portal portal, Notification notification) { + Object (portal: portal, notification: notification); + } + + construct { + child = new Widget (notification); + + insert_action_group (Notification.ACTION_GROUP_NAME, portal.actions); + } +} diff --git a/src/Notification/Widget.vala b/src/Notification/Widgets/Widget.vala similarity index 100% rename from src/Notification/Widget.vala rename to src/Notification/Widgets/Widget.vala diff --git a/src/Notification/Window.vala b/src/Notification/Window.vala deleted file mode 100644 index 9fa6f24a..00000000 --- a/src/Notification/Window.vala +++ /dev/null @@ -1,31 +0,0 @@ -/* -* SPDX-License-Identifier: GPL-3.0-or-later -* SPDX-FileCopyrightText: {{YEAR}} {{DEVELOPER_NAME}} <{{DEVELOPER_EMAIL}}> -*/ - -public class Notification.MainWindow : Gtk.Window { - public Portal portal { get; construct; } - - public MainWindow (Portal portal) { - Object ( - default_height: 500, - default_width: 500, - title: _("My App Name"), - portal: portal - ); - } - - construct { - var list_box = new Gtk.ListBox (); - list_box.bind_model (portal.notifications, create_widget_func); - - child = list_box; - titlebar = new Gtk.Grid () { visible = false }; - } - - private Gtk.Widget create_widget_func (Object obj) { - var notification = (Notification) obj; - var widget = new Widget (notification); - return widget; - } -} diff --git a/src/meson.build b/src/meson.build index c94da5a0..82fa8020 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,11 +9,12 @@ executable( 'Background/NotificationRequest.vala', 'Background/Portal.vala', 'Notification/ActionGroup.vala', + 'Notification/BubbleManager.vala', 'Notification/DBus.vala', 'Notification/Notification.vala', 'Notification/Portal.vala', - 'Notification/Widget.vala', - 'Notification/Window.vala', + 'Notification/Widgets/Bubble.vala', + 'Notification/Widgets/Widget.vala', 'ScreenCast/MonitorTracker/Interface.vala', 'ScreenCast/MonitorTracker/Monitor.vala', 'ScreenCast/MonitorTracker/MonitorTracker.vala', From 8e027d4312d2ec16659ba6781e2c4a3924b0fb16 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 6 Jan 2025 00:14:20 +0100 Subject: [PATCH 07/16] Prepare moving all widgets to indicator --- src/Notification/ActionGroup.vala | 10 +- src/Notification/DBus.vala | 16 ++- src/Notification/Notification.vala | 149 +++++++++------------------ src/Notification/Portal.vala | 2 +- src/Notification/Widgets/Widget.vala | 2 +- 5 files changed, 62 insertions(+), 117 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index ca1bdc92..678789e6 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -17,11 +17,10 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { for (uint i = 0; i < portal.notifications.n_items; i++) { var notification = (Notification) portal.notifications.get_item (i); + builder.add (notification.dismiss_action_name); builder.add (notification.default_action_name); - builder.add (notification.close_action_name); - for (uint j = 0; j < notification.buttons.n_items; j++) { - var button = (Notification.Button) notification.buttons.get_item (j); + foreach (var button in notification.buttons) { builder.add (button.action_name); } } @@ -97,7 +96,7 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { Notification? notification = null; for (uint i = 0; i < portal.notifications.n_items; i++) { var n = (Notification) portal.notifications.get_item (i); - if (n.id == internal_id) { + if (n.internal_id == internal_id) { notification = n; break; } @@ -109,8 +108,7 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { } if (type == "action") { - for (uint i = 0; i < notification.buttons.n_items; i++) { - var button = (Notification.Button) notification.buttons.get_item (i); + foreach (var button in notification.buttons) { if (button.action_name == action_name) { parameter_type = button.action_target.get_type (); return true; diff --git a/src/Notification/DBus.vala b/src/Notification/DBus.vala index 5526bf8e..c6498c3a 100644 --- a/src/Notification/DBus.vala +++ b/src/Notification/DBus.vala @@ -1,6 +1,6 @@ [DBus (name = "io.elementary.portal.NotificationProvider")] public class Notification.DBusProvider : Object { - public signal void items_changed (uint pos, uint removed, HashTable[] added); + public signal void items_changed (uint pos, uint removed, uint added); [DBus (visible = false)] public Portal portal { get; construct; } @@ -10,16 +10,14 @@ public class Notification.DBusProvider : Object { } construct { - portal.notifications.items_changed.connect (on_items_changed); + portal.notifications.items_changed.connect ((pos, removed, added) => items_changed (pos, removed, added)); } - private void on_items_changed (uint pos, uint removed, uint added) { - HashTable[] added_notifications = new HashTable[added]; - - for (uint i = 0; i < added; i++) { - added_notifications[i] = ((Notification) portal.notifications.get_item (pos + i)).data; - } + public uint get_n_items () throws DBusError, IOError { + return portal.notifications.n_items; + } - items_changed (pos, removed, added_notifications); + public Notification.Data get_notification (uint index) throws DBusError, IOError { + return ((Notification) portal.notifications.get_item (index)).data; } } diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index 6e973de9..dc002e82 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -5,12 +5,6 @@ public class Notification.Notification : GLib.Object { public const string ACTION_FORMAT = "%s+action+%s"; // interal id, action id public const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action id - public class Button : Object { - public string label; - public string action_name; - public Variant? action_target = null; - } - [Flags] public enum DisplayHint { TRANSIENT, @@ -21,114 +15,69 @@ public class Notification.Notification : GLib.Object { SHOW_AS_NEW } - public string id { get; construct; } - - public string app_id { get; construct; } - - public HashTable data { get; construct; } - - /** - * The title of the notification, always uses markup. - */ - public string title { get; private set; } - - /** - * The body of the notification, always uses markup. - */ - public string body { get; private set; } - - public string time { get; private set; } // with 60 second timeout - - public Icon primary_icon { get; private set; } - public Icon? secondary_icon { get; private set; } - - public NotificationPriority priority { get; private set; default = NORMAL; } - - public string close_action_name { owned get { return INTERNAL_ACTION_FORMAT.printf (id, "dismiss"); } } - - public string default_action_name { get; construct; } - public Variant? default_action_target { get; construct; } - - public ListStore buttons { get; private set; } - - public DisplayHint display_hint { get; private set; } - - public Notification (string app_id, string id, HashTable data) { - Object (app_id: app_id, id: Portal.ID_FORMAT.printf (app_id, id), data: data); + public struct Button { + public string label; + public string action_name; + public Variant? action_target; } - construct { - buttons = new ListStore (typeof (Button)); - - if ("title" in data) { - title = data["title"].get_string (); - } - - if ("body" in data) { - body = data["body"].get_string (); - } - - if ("markup-body" in data) { - body = data["markup-body"].get_string (); - } - - var desktop_app_info = new DesktopAppInfo (app_id + ".desktop"); - - primary_icon = desktop_app_info.get_icon (); - - if ("icon" in data) { - // do some shit - } - - if ("priority" in data) { - var priority = data["priority"].get_string (); - - switch (priority) { - case "low": - this.priority = LOW; - break; - - case "normal": - default: - this.priority = NORMAL; - break; + public struct Data { + public string internal_id; + public string app_id; + public string dismiss_action_name; + public string default_action_name; + public Variant? default_action_target; + public Button[] buttons; + public DisplayHint display_hint; + public HashTable raw_data; + + public Data (string internal_id, string app_id, HashTable raw_data) { + if ("default-action" in raw_data) { + default_action_name = ACTION_FORMAT.printf (internal_id, raw_data["default-action"].get_string ()); + + if ("default-action-target" in raw_data) { + default_action_target = raw_data["default-action-target"]; + } + } else { + default_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "default"); } - } - if ("default-action" in data) { - default_action_name = ACTION_FORMAT.printf (id, data["default-action"].get_string ()); + if ("buttons" in raw_data) { + var raw_buttons = (HashTable[]) raw_data["buttons"]; - if ("default-action-target" in data) { - default_action_target = data["default-action-target"]; - } - } else { - default_action_name = INTERNAL_ACTION_FORMAT.printf (id, "default"); - } + foreach (var button_data in raw_buttons) { + var button = Button (); - if ("buttons" in data) { - var buttons = (HashTable[]) data["buttons"]; + if ("label" in button_data) { + button.label = button_data["label"].get_string (); + } - foreach (var button_data in buttons) { - var button = new Button (); - - if ("label" in button_data) { - button.label = button_data["label"].get_string (); - } + if ("action" in button_data) { + button.action_name = ACTION_FORMAT.printf (internal_id, button_data["action"].get_string ()); + } - if ("action" in button_data) { - button.action_name = ACTION_FORMAT.printf (id, button_data["action"].get_string ()); - } + if ("action-target" in button_data) { + button.action_target = button_data["action-target"]; + } - if ("action-target" in button_data) { - button.action_target = button_data["action-target"]; + buttons += button; } - - this.buttons.append (button); } } } - public void play_sound () { + public Data data { get; construct; } + + public string internal_id { get { return data.internal_id; } } + + public string dismiss_action_name { get { return data.dismiss_action_name; } } + public string default_action_name { get { return data.default_action_name; } } + public Variant? default_action_target { get { return data.default_action_target; } } + public Button[] buttons { get { return data.buttons; } } + + public DisplayHint display_hint { get { return data.display_hint; } } + public Notification (string internal_id, string app_id, HashTable raw_data) { + Object (data: Data (internal_id, app_id, raw_data)); } } diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 1d17fef0..50265ac8 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -58,7 +58,7 @@ public class Notification.Portal : Object { internal void replace_notification (string internal_id, Notification? replacement) { for (int i = 0; i < notifications.n_items; i++) { var notification = (Notification) notifications.get_object (i); - if (notification.id == internal_id) { + if (notification.internal_id == internal_id) { if (replacement == null) { // Just remove and return notifications.remove (i); return; diff --git a/src/Notification/Widgets/Widget.vala b/src/Notification/Widgets/Widget.vala index 70e954d8..b160ba99 100644 --- a/src/Notification/Widgets/Widget.vala +++ b/src/Notification/Widgets/Widget.vala @@ -59,7 +59,7 @@ public class Notification.Widget : Granite.Bin { selection_mode = NONE, homogeneous = true }; - button_box.bind_model (notification.buttons, create_button_func); + // button_box.bind_model (notification.buttons, create_button_func); var grid = new Gtk.Grid (); grid.attach (icon_overlay, 0, 0, 1, 2); From 2ee78178e5cded4ca1f5f2c62d4b919be62ed98e Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 6 Jan 2025 00:16:44 +0100 Subject: [PATCH 08/16] Cleanup --- src/Notification/Notification.vala | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index dc002e82..bd80a990 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -19,6 +19,20 @@ public class Notification.Notification : GLib.Object { public string label; public string action_name; public Variant? action_target; + + public Button (HashTable data) { + if ("label" in data) { + label = data["label"].get_string (); + } + + if ("action" in data) { + action_name = ACTION_FORMAT.printf (internal_id, data["action"].get_string ()); + } + + if ("action-target" in data) { + action_target = data["action-target"]; + } + } } public struct Data { @@ -45,22 +59,8 @@ public class Notification.Notification : GLib.Object { if ("buttons" in raw_data) { var raw_buttons = (HashTable[]) raw_data["buttons"]; - foreach (var button_data in raw_buttons) { - var button = Button (); - - if ("label" in button_data) { - button.label = button_data["label"].get_string (); - } - - if ("action" in button_data) { - button.action_name = ACTION_FORMAT.printf (internal_id, button_data["action"].get_string ()); - } - - if ("action-target" in button_data) { - button.action_target = button_data["action-target"]; - } - - buttons += button; + foreach (var raw_button in raw_buttons) { + buttons += Button (raw_button); } } } From 5ae3cce36c5944df241686ff85726eaf89e2d84d Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Mon, 6 Jan 2025 00:23:05 +0100 Subject: [PATCH 09/16] Fix build --- src/Notification/Notification.vala | 8 +++++--- src/Notification/Widgets/Widget.vala | 8 -------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index bd80a990..73d080c0 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -20,7 +20,7 @@ public class Notification.Notification : GLib.Object { public string action_name; public Variant? action_target; - public Button (HashTable data) { + public Button (string internal_id, HashTable data) { if ("label" in data) { label = data["label"].get_string (); } @@ -59,8 +59,10 @@ public class Notification.Notification : GLib.Object { if ("buttons" in raw_data) { var raw_buttons = (HashTable[]) raw_data["buttons"]; - foreach (var raw_button in raw_buttons) { - buttons += Button (raw_button); + buttons = new Button[raw_buttons.length]; + + for (int i = 0; i < raw_buttons.length; i++) { + buttons[i] = Button (internal_id, raw_buttons[i]); } } } diff --git a/src/Notification/Widgets/Widget.vala b/src/Notification/Widgets/Widget.vala index b160ba99..d54713ab 100644 --- a/src/Notification/Widgets/Widget.vala +++ b/src/Notification/Widgets/Widget.vala @@ -75,14 +75,6 @@ public class Notification.Widget : Granite.Bin { add_controller (gesture_click); } - private Gtk.Widget create_button_func (Object obj) { - var button = (Notification.Button) obj; - return new Gtk.Button.with_label (button.label) { - action_name = Notification.ACTION_PREFIX + button.action_name, - action_target = button.action_target - }; - } - private void bind_with_visible (string property, Gtk.Widget widget, string widget_property) { notification.bind_property (property, widget, widget_property, SYNC_CREATE); } From a56821aa3b3aed4de0f8bd663720322114ef2475 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Tue, 7 Jan 2025 16:40:43 +0100 Subject: [PATCH 10/16] Pretty solid api --- src/Notification/ActionGroup.vala | 4 ++-- src/Notification/Notification.vala | 28 +++++++++++++++++++++------- src/Notification/Portal.vala | 20 +++++++++++++++++--- src/XdgDesktopPortalPantheon.vala | 2 +- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index 678789e6..3797f96f 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -110,14 +110,14 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { if (type == "action") { foreach (var button in notification.buttons) { if (button.action_name == action_name) { - parameter_type = button.action_target.get_type (); + parameter_type = button.action_target.length > 0 ? button.action_target[0].get_type () : null; return true; } } } else { switch (action_name) { case "default": - parameter_type = notification.default_action_target != null ? notification.default_action_target.get_type () : null; + parameter_type = notification.default_action_target.length > 0 ? notification.default_action_target[0].get_type () : null; return true; case "dismiss": diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index 73d080c0..11578b02 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -18,7 +18,7 @@ public class Notification.Notification : GLib.Object { public struct Button { public string label; public string action_name; - public Variant? action_target; + public Variant[] action_target; public Button (string internal_id, HashTable data) { if ("label" in data) { @@ -30,30 +30,40 @@ public class Notification.Notification : GLib.Object { } if ("action-target" in data) { - action_target = data["action-target"]; + action_target = { data["action-target"] }; + } else { + action_target = {}; } } } public struct Data { public string internal_id; + public HashTable raw_data; public string app_id; public string dismiss_action_name; public string default_action_name; - public Variant? default_action_target; + public Variant[] default_action_target; public Button[] buttons; public DisplayHint display_hint; - public HashTable raw_data; - public Data (string internal_id, string app_id, HashTable raw_data) { + public Data (string _internal_id, string _app_id, HashTable _raw_data) { + internal_id = _internal_id; + raw_data = _raw_data; + app_id = _app_id; + dismiss_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "dismiss"); + if ("default-action" in raw_data) { default_action_name = ACTION_FORMAT.printf (internal_id, raw_data["default-action"].get_string ()); if ("default-action-target" in raw_data) { - default_action_target = raw_data["default-action-target"]; + default_action_target = { raw_data["default-action-target"] }; + } else { + default_action_target = {}; } } else { default_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "default"); + default_action_target = {}; } if ("buttons" in raw_data) { @@ -64,7 +74,11 @@ public class Notification.Notification : GLib.Object { for (int i = 0; i < raw_buttons.length; i++) { buttons[i] = Button (internal_id, raw_buttons[i]); } + } else { + buttons = new Button[0]; } + + display_hint = 0; } } @@ -74,7 +88,7 @@ public class Notification.Notification : GLib.Object { public string dismiss_action_name { get { return data.dismiss_action_name; } } public string default_action_name { get { return data.default_action_name; } } - public Variant? default_action_target { get { return data.default_action_target; } } + public Variant[] default_action_target { get { return data.default_action_target; } } public Button[] buttons { get { return data.buttons; } } public DisplayHint display_hint { get { return data.display_hint; } } diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 50265ac8..2a02b729 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -19,13 +19,19 @@ public class Notification.Portal : Object { public HashTable supported_options { get; construct; } + [DBus (visible = false)] + public DBusConnection connection { get; construct; } + [DBus (visible = false)] public ListStore notifications { get; construct; } [DBus (visible = false)] public ActionGroup actions { get; construct; } private DBusProvider dbus_provider; - private BubbleManager bubble_manager; + + public Portal (DBusConnection connection) { + Object (connection: connection); + } construct { supported_options = new HashTable (str_hash, str_equal); @@ -34,7 +40,13 @@ public class Notification.Portal : Object { actions = new ActionGroup (this); dbus_provider = new DBusProvider (this); - bubble_manager = new BubbleManager (this); + + try { + connection.register_object ("/io/elementary/portal/NotificationProvider", dbus_provider); + connection.export_action_group ("/io/elementary/portal/NotificationProvider", actions); + } catch (Error e) { + warning ("Failed to register provider: %s", e.message); + } } public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { @@ -71,6 +83,8 @@ public class Notification.Portal : Object { } } - notifications.splice (0, 0, { replacement }); + if (replacement != null) { + notifications.splice (0, 0, { replacement }); + } } } diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index b50bac6a..d2205e3e 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -41,7 +41,7 @@ private void on_bus_acquired (DBusConnection connection, string name) { connection.register_object ("/org/freedesktop/portal/desktop", new Background.Portal (connection)); debug ("Background Portal registered!"); - connection.register_object ("/org/freedesktop/portal/desktop", new Notification.Portal ()); + connection.register_object ("/org/freedesktop/portal/desktop", new Notification.Portal (connection)); debug ("Notification Portal registered!"); connection.register_object ("/org/freedesktop/portal/desktop", new Screenshot.Portal (connection)); From e02777e029bade0d3339f98f17355b64f8bba04c Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 13 Apr 2025 19:26:10 +0200 Subject: [PATCH 11/16] Minor adjustments --- src/Notification/ActionGroup.vala | 8 +-- src/Notification/BubbleManager.vala | 24 --------- src/Notification/Notification.vala | 13 +++++ src/Notification/Portal.vala | 6 +-- src/Notification/Widgets/Bubble.vala | 14 ----- src/Notification/Widgets/Widget.vala | 81 ---------------------------- src/meson.build | 3 -- 7 files changed, 17 insertions(+), 132 deletions(-) delete mode 100644 src/Notification/BubbleManager.vala delete mode 100644 src/Notification/Widgets/Bubble.vala delete mode 100644 src/Notification/Widgets/Widget.vala diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index 3797f96f..96503edd 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -16,13 +16,7 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { for (uint i = 0; i < portal.notifications.n_items; i++) { var notification = (Notification) portal.notifications.get_item (i); - - builder.add (notification.dismiss_action_name); - builder.add (notification.default_action_name); - - foreach (var button in notification.buttons) { - builder.add (button.action_name); - } + builder.addv (notification.get_actions ()); } return builder.end (); diff --git a/src/Notification/BubbleManager.vala b/src/Notification/BubbleManager.vala deleted file mode 100644 index 53222ee5..00000000 --- a/src/Notification/BubbleManager.vala +++ /dev/null @@ -1,24 +0,0 @@ - - -public class Notification.BubbleManager : Object { - public Portal portal { get; construct; } - - public BubbleManager (Portal portal) { - Object (portal: portal); - } - - construct { - portal.notifications.items_changed.connect (on_items_changed); - } - - private void on_items_changed (uint pos, uint removed, uint added) { - if (pos != 0 || removed != 0) { - return; - } - - var added_notification = (Notification) portal.notifications.get_item (pos); - - var bubble = new Bubble (portal, added_notification); - bubble.present (); - } -} diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index 11578b02..a8abd5f3 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -96,4 +96,17 @@ public class Notification.Notification : GLib.Object { public Notification (string internal_id, string app_id, HashTable raw_data) { Object (data: Data (internal_id, app_id, raw_data)); } + + public string[] get_actions () { + string[] actions = new string[data.buttons.length + 2]; + + actions[0] = dismiss_action_name; + actions[1] = default_action_name; + + for (int i = 0; i < data.buttons.length; i++) { + actions[i + 2] = data.buttons[i].action_name; + } + + return actions; + } } diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 2a02b729..45fe24aa 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -7,8 +7,9 @@ * api, tracks currently active notifications and holds the other parts. * The {@link ActionGroup} handles all action logic for notifications. It automatically exposes all actions * for all available notifications and handles the activation of these actions (by talking to #this). + * It's also exported on the bus for usage by the indicator. * The {@link BubbleManager} is responsible for showing the notifications to the user in a bubble. - * The {@link DBusProvider} is responsible for exposing the notifications to the DBus for consumption by the indicator. It also exports the {@link actions}. + * The {@link DBusProvider} is responsible for exposing the notifications to the DBus for consumption by the indicator. * Both {@link BubbleManager} and {@link DBusProvider} use the {@link actions} for all interaction (dismissing, activating actions). */ [DBus (name = "org.freedesktop.impl.portal.Notification")] @@ -24,9 +25,8 @@ public class Notification.Portal : Object { [DBus (visible = false)] public ListStore notifications { get; construct; } - [DBus (visible = false)] - public ActionGroup actions { get; construct; } + private ActionGroup actions; private DBusProvider dbus_provider; public Portal (DBusConnection connection) { diff --git a/src/Notification/Widgets/Bubble.vala b/src/Notification/Widgets/Bubble.vala deleted file mode 100644 index 41a23454..00000000 --- a/src/Notification/Widgets/Bubble.vala +++ /dev/null @@ -1,14 +0,0 @@ -public class Notification.Bubble : Gtk.Window { - public Portal portal { get; construct; } - public Notification notification { get; construct; } - - public Bubble (Portal portal, Notification notification) { - Object (portal: portal, notification: notification); - } - - construct { - child = new Widget (notification); - - insert_action_group (Notification.ACTION_GROUP_NAME, portal.actions); - } -} diff --git a/src/Notification/Widgets/Widget.vala b/src/Notification/Widgets/Widget.vala deleted file mode 100644 index d54713ab..00000000 --- a/src/Notification/Widgets/Widget.vala +++ /dev/null @@ -1,81 +0,0 @@ -public class Notification.Widget : Granite.Bin { - public Notification notification { get; construct; } - - public Gtk.Label time_label { get; construct; } - - private Gtk.FlowBox button_box; - - public Widget (Notification notification) { - Object (notification: notification); - } - - construct { - var primary_icon = new Gtk.Image (); - notification.bind_property ("primary-icon", primary_icon, "gicon", SYNC_CREATE); - - var secondary_icon = new Gtk.Image (); - bind_with_visible ("secondary-icon", secondary_icon, "gicon"); - - var icon_overlay = new Gtk.Overlay () { - child = primary_icon - }; - icon_overlay.add_overlay (secondary_icon); - - var title_label = new Gtk.Label (null) { - halign = START, - margin_start = 12, - margin_end = 12, - margin_top = 12, - margin_bottom = 6, - wrap = true, - max_width_chars = 50, - ellipsize = END, - use_markup = true - }; - notification.bind_property ("title", title_label, "label", SYNC_CREATE); - - time_label = new Gtk.Label (null); - notification.bind_property ("time", time_label, "label", SYNC_CREATE); - - var body_label = new Gtk.Label (null) { - halign = START, - margin_start = 12, - margin_end = 12, - margin_top = 12, - margin_bottom = 6, - wrap = true, - max_width_chars = 50, - ellipsize = END, - use_markup = true - }; - notification.bind_property ("body", body_label, "label", SYNC_CREATE); - - button_box = new Gtk.FlowBox () { - halign = END, - margin_start = 12, - margin_end = 12, - margin_top = 6, - margin_bottom = 6, - selection_mode = NONE, - homogeneous = true - }; - // button_box.bind_model (notification.buttons, create_button_func); - - var grid = new Gtk.Grid (); - grid.attach (icon_overlay, 0, 0, 1, 2); - grid.attach (title_label, 1, 0, 1, 1); - grid.attach (time_label, 2, 0, 1, 1); - grid.attach (body_label, 1, 1, 2, 1); - grid.attach (button_box, 1, 2, 2, 1); - - child = grid; - - var gesture_click = new Gtk.GestureClick (); - gesture_click.pressed.connect (() => activate_action_variant (notification.default_action_name, notification.default_action_target)); - add_controller (gesture_click); - } - - private void bind_with_visible (string property, Gtk.Widget widget, string widget_property) { - notification.bind_property (property, widget, widget_property, SYNC_CREATE); - } -} diff --git a/src/meson.build b/src/meson.build index 82fa8020..237afee7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,12 +9,9 @@ executable( 'Background/NotificationRequest.vala', 'Background/Portal.vala', 'Notification/ActionGroup.vala', - 'Notification/BubbleManager.vala', 'Notification/DBus.vala', 'Notification/Notification.vala', 'Notification/Portal.vala', - 'Notification/Widgets/Bubble.vala', - 'Notification/Widgets/Widget.vala', 'ScreenCast/MonitorTracker/Interface.vala', 'ScreenCast/MonitorTracker/Monitor.vala', 'ScreenCast/MonitorTracker/MonitorTracker.vala', From 7a2d1c47de10252fd7c134e46874715af8e2a7f0 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sun, 13 Apr 2025 19:35:16 +0200 Subject: [PATCH 12/16] Fix ids and actions --- src/Notification/ActionGroup.vala | 16 ++++++++++++++++ src/Notification/Portal.vala | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index 96503edd..2c73187a 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -11,6 +11,22 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { Object (portal: portal); } + construct { + portal.notifications.items_changed.connect (on_items_changed); + } + + private void on_items_changed (uint pos, uint removed, uint added) { + for (uint i = pos; i < pos + added; i++) { + var notification = (Notification) portal.notifications.get_item (i); + + foreach (var action in notification.get_actions ()) { + action_added (action); + } + } + + //TODO: Maybe handle remove + } + public string[] list_actions () { var builder = new StrvBuilder (); diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 45fe24aa..cf2e122a 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -50,8 +50,8 @@ public class Notification.Portal : Object { } public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { - var internal_id = ID_FORMAT.printf (app_id, id); - var notification = new Notification (app_id, id, data); + var internal_id = ID_FORMAT.printf (app_id, id != "" ? id : Uuid.string_random ()); + var notification = new Notification (internal_id, app_id, data); replace_notification (internal_id, notification); } From e83287dbae4ea75372621769971af0f816bfd30b Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Tue, 15 Apr 2025 13:43:42 +0200 Subject: [PATCH 13/16] Fix some action stuff --- src/Notification/ActionGroup.vala | 88 +++++++++++------------------ src/Notification/Notification.vala | 90 ++++++++++++++++++++++++------ src/Notification/Portal.vala | 44 ++++++++------- 3 files changed, 130 insertions(+), 92 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index 2c73187a..34176ddb 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -50,31 +50,44 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { var type = parts[1]; var action_name = parts[2]; - var id_parts = internal_id.split (":", 2); + var notification = Notification.get_for_internal_id (internal_id); - if (id_parts.length != 2) { - warning ("Invalid internal id: %s", internal_id); + if (notification == null) { + warning ("Notification not found: %s", internal_id); return; } - var app_id = id_parts[0]; - var notification_id = id_parts[1]; + var app_id = notification.app_id; + var id = notification.id; + + switch (type) { + case Notification.ACTION_TYPE_ACTION: + var paramters = target != null ? new Variant[] { target } : new Variant[0]; + portal.action_invoked (app_id, id, action_name, paramters); + break; - if (type == "action") { - portal.action_invoked (app_id, notification_id, action_name, { target }); - } else { - switch (action_name) { - case "default": - // launch - break; + case Notification.ACTION_TYPE_INTERNAL: + switch (action_name) { + case Notification.ACTION_DEFAULT: + // launch + warning ("Launched"); + break; - case "dismiss": - portal.replace_notification (internal_id, null); - break; + case Notification.ACTION_DISMISS: + break; - default: - break; - } + default: + return; + } + break; + + default: + return; + } + + uint position; + if (portal.notifications.find (notification, out position)) { + portal.notifications.remove (position); } } @@ -99,47 +112,14 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { return false; } - var internal_id = parts[0]; - var type = parts[1]; - var action_name = parts[2]; - - Notification? notification = null; - for (uint i = 0; i < portal.notifications.n_items; i++) { - var n = (Notification) portal.notifications.get_item (i); - if (n.internal_id == internal_id) { - notification = n; - break; - } - } + var notification = Notification.get_for_internal_id (parts[0]); if (notification == null) { - warning ("Notification not found: %s", internal_id); + warning ("Notification not found: %s", parts[0]); return false; } - if (type == "action") { - foreach (var button in notification.buttons) { - if (button.action_name == action_name) { - parameter_type = button.action_target.length > 0 ? button.action_target[0].get_type () : null; - return true; - } - } - } else { - switch (action_name) { - case "default": - parameter_type = notification.default_action_target.length > 0 ? notification.default_action_target[0].get_type () : null; - return true; - - case "dismiss": - parameter_type = null; - return true; - - default: - return false; - } - } - - return true; + return notification.query_action (name, out enabled, out parameter_type, out state_type, out state_hint, out state); } public void change_action_state (string action_name, Variant value) { } diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index a8abd5f3..e92a9172 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -1,9 +1,12 @@ public class Notification.Notification : GLib.Object { - public const string ACTION_GROUP_NAME = "action"; - public const string ACTION_PREFIX = ACTION_GROUP_NAME + "."; - public const string ACTION_FORMAT = "%s+action+%s"; // interal id, action id - public const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action id + public const string ACTION_TYPE_ACTION = "action"; + public const string ACTION_TYPE_INTERNAL = "internal"; + public const string ACTION_DISMISS = "dismiss"; + public const string ACTION_DEFAULT = "default"; + + private const string ACTION_FORMAT = "%s+action+%s"; // interal id, action name + private const string INTERNAL_ACTION_FORMAT = "%s+internal+%s"; // interal id, action name [Flags] public enum DisplayHint { @@ -38,7 +41,6 @@ public class Notification.Notification : GLib.Object { } public struct Data { - public string internal_id; public HashTable raw_data; public string app_id; public string dismiss_action_name; @@ -47,8 +49,7 @@ public class Notification.Notification : GLib.Object { public Button[] buttons; public DisplayHint display_hint; - public Data (string _internal_id, string _app_id, HashTable _raw_data) { - internal_id = _internal_id; + public Data (string internal_id, string _app_id, HashTable _raw_data) { raw_data = _raw_data; app_id = _app_id; dismiss_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "dismiss"); @@ -82,26 +83,39 @@ public class Notification.Notification : GLib.Object { } } - public Data data { get; construct; } + private static HashTable notifications_by_internal_id = new HashTable (str_hash, str_equal); + private static uint internal_ids = 0; + + public static Notification? get_for_internal_id (string internal_id) { + return notifications_by_internal_id[internal_id]; + } - public string internal_id { get { return data.internal_id; } } + public Data data { get; construct; } - public string dismiss_action_name { get { return data.dismiss_action_name; } } - public string default_action_name { get { return data.default_action_name; } } - public Variant[] default_action_target { get { return data.default_action_target; } } - public Button[] buttons { get { return data.buttons; } } + public string internal_id { private get; construct; } + public string app_id { get; construct; } + public string id { get; construct; } public DisplayHint display_hint { get { return data.display_hint; } } - public Notification (string internal_id, string app_id, HashTable raw_data) { - Object (data: Data (internal_id, app_id, raw_data)); + public Notification (string app_id, string id, HashTable raw_data) { + var internal_id = "%u".printf (internal_ids++); + Object (internal_id: internal_id, app_id: app_id, id: id, data: Data (internal_id, app_id, raw_data)); + } + + construct { + notifications_by_internal_id[internal_id] = this; + } + + ~Notification () { + notifications_by_internal_id.remove (internal_id); } public string[] get_actions () { string[] actions = new string[data.buttons.length + 2]; - actions[0] = dismiss_action_name; - actions[1] = default_action_name; + actions[0] = data.dismiss_action_name; + actions[1] = data.default_action_name; for (int i = 0; i < data.buttons.length; i++) { actions[i + 2] = data.buttons[i].action_name; @@ -109,4 +123,46 @@ public class Notification.Notification : GLib.Object { return actions; } + + public bool query_action ( + string name, + out bool enabled, + out unowned VariantType parameter_type, + out unowned VariantType state_type, + out Variant state_hint, + out Variant state + ) { + enabled = true; + parameter_type = null; + state_type = null; + state_hint = null; + state = null; + + if (name == data.dismiss_action_name) { + parameter_type = null; + return true; + } + + if (name == data.default_action_name) { + parameter_type = variant_type_from_maybe_array (data.default_action_target); + return true; + } + + foreach (var button in data.buttons) { + if (button.action_name == name) { + parameter_type = variant_type_from_maybe_array (button.action_target); + return true; + } + } + + return false; + } + + private unowned VariantType? variant_type_from_maybe_array (Variant[] arr) { + if (arr.length == 0) { + return null; + } else { + return arr[0].get_type (); + } + } } diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index cf2e122a..838cf8af 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -19,9 +19,10 @@ public class Notification.Portal : Object { public signal void action_invoked (string app_id, string id, string action_name, Variant[] parameters); public HashTable supported_options { get; construct; } + public uint version { get; default = 2; } [DBus (visible = false)] - public DBusConnection connection { get; construct; } + public DBusConnection connection { private get; construct; } [DBus (visible = false)] public ListStore notifications { get; construct; } @@ -50,16 +51,12 @@ public class Notification.Portal : Object { } public void add_notification (string app_id, string id, HashTable data) throws DBusError, IOError { - var internal_id = ID_FORMAT.printf (app_id, id != "" ? id : Uuid.string_random ()); - var notification = new Notification (internal_id, app_id, data); - - replace_notification (internal_id, notification); + var notification = new Notification (app_id, id, data); + replace_notification (app_id, id, notification); } public void remove_notification (string app_id, string id) throws DBusError, IOError { - var internal_id = ID_FORMAT.printf (app_id, id); - - replace_notification (internal_id, null); + replace_notification (app_id, id, null); } /** @@ -67,24 +64,29 @@ public class Notification.Portal : Object { * If SHOW_AS_NEW is set in the display hint of the replacement, it will be added at the front instead of at the same position. * If no notification with the given id is found, and the replacement is not null, the replacement will be added at the front. */ - internal void replace_notification (string internal_id, Notification? replacement) { - for (int i = 0; i < notifications.n_items; i++) { - var notification = (Notification) notifications.get_object (i); - if (notification.internal_id == internal_id) { - if (replacement == null) { // Just remove and return - notifications.remove (i); - return; - } else if (SHOW_AS_NEW in replacement.display_hint) { // Remove but don't return because we want to add the replacement as if it was a new notification - notifications.remove (i); - } else { // Replace and return - notifications.splice (i, 1, { replacement }); - return; - } + internal void replace_notification (string app_id, string id, Notification? replacement) { + for (uint i = 0; i < (id == "" ? 0 : notifications.n_items); i++) { + var notification = (Notification) notifications.get_item (i); + if (notification.app_id != app_id || notification.id != id) { + continue; + } + + if (replacement == null) { // Just remove and return + notifications.remove (i); + return; + } else if (SHOW_AS_NEW in replacement.display_hint) { // Remove but don't return because we want to add the replacement as if it was a new notification + notifications.remove (i); + break; + } else { // Replace and return + notifications.splice (i, 1, { replacement }); + return; } } if (replacement != null) { notifications.splice (0, 0, { replacement }); + } else { + warning ("This shouldn't be reached."); } } } From f2ff2cfe39d70157c908dd8631feb402297d2652 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Tue, 15 Apr 2025 23:50:07 +0200 Subject: [PATCH 14/16] Add timestamps --- src/Notification/Notification.vala | 11 ++++++++-- src/Notification/Portal.vala | 33 ++++++++++++++++-------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index e92a9172..b99f9ec7 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -41,6 +41,7 @@ public class Notification.Notification : GLib.Object { } public struct Data { + public int64 timestamp; public HashTable raw_data; public string app_id; public string dismiss_action_name; @@ -50,6 +51,7 @@ public class Notification.Notification : GLib.Object { public DisplayHint display_hint; public Data (string internal_id, string _app_id, HashTable _raw_data) { + timestamp = new DateTime.now_utc ().to_unix (); raw_data = _raw_data; app_id = _app_id; dismiss_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "dismiss"); @@ -90,13 +92,14 @@ public class Notification.Notification : GLib.Object { return notifications_by_internal_id[internal_id]; } - public Data data { get; construct; } + private Data _data; + public Data data { get { return _data; } construct { _data = value; } } public string internal_id { private get; construct; } public string app_id { get; construct; } public string id { get; construct; } - public DisplayHint display_hint { get { return data.display_hint; } } + public DisplayHint display_hint { get { return _data.display_hint; } } public Notification (string app_id, string id, HashTable raw_data) { var internal_id = "%u".printf (internal_ids++); @@ -165,4 +168,8 @@ public class Notification.Notification : GLib.Object { return arr[0].get_type (); } } + + public void replace_timestamp (Notification old_notification) { + _data.timestamp = old_notification._data.timestamp; + } } diff --git a/src/Notification/Portal.vala b/src/Notification/Portal.vala index 838cf8af..be64286a 100644 --- a/src/Notification/Portal.vala +++ b/src/Notification/Portal.vala @@ -65,21 +65,24 @@ public class Notification.Portal : Object { * If no notification with the given id is found, and the replacement is not null, the replacement will be added at the front. */ internal void replace_notification (string app_id, string id, Notification? replacement) { - for (uint i = 0; i < (id == "" ? 0 : notifications.n_items); i++) { - var notification = (Notification) notifications.get_item (i); - if (notification.app_id != app_id || notification.id != id) { - continue; - } - - if (replacement == null) { // Just remove and return - notifications.remove (i); - return; - } else if (SHOW_AS_NEW in replacement.display_hint) { // Remove but don't return because we want to add the replacement as if it was a new notification - notifications.remove (i); - break; - } else { // Replace and return - notifications.splice (i, 1, { replacement }); - return; + if (id != "") { + for (uint i = 0; i < notifications.n_items; i++) { + var notification = (Notification) notifications.get_item (i); + if (notification.app_id != app_id || notification.id != id) { + continue; + } + + if (replacement == null) { // Just remove and return + notifications.remove (i); + return; + } else if (SHOW_AS_NEW in replacement.display_hint) { // Remove but don't return because we want to add the replacement as if it was a new notification + notifications.remove (i); + break; + } else { // Replace and return + replacement.replace_timestamp (notification); + notifications.splice (i, 1, { replacement }); + return; + } } } From b437310ad442f4ff1eecdb4f306b4573d67eab89 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 17 Apr 2025 01:00:31 +0200 Subject: [PATCH 15/16] Introduce xdg activation support --- src/Notification/ActionGroup.vala | 30 ++++++++++++++++++++++---- src/Notification/Notification.vala | 34 ++++++++++++------------------ 2 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index 34176ddb..439ff320 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -5,6 +5,9 @@ * close the notification or launch the application. */ public class Notification.ActionGroup : Object, GLib.ActionGroup { + private const string TARGET_TYPE_STRING = "(sv)"; + public static VariantType target_type = new VariantType (TARGET_TYPE_STRING); + public Portal portal { get; construct; } public ActionGroup (Portal portal) { @@ -19,7 +22,7 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { for (uint i = pos; i < pos + added; i++) { var notification = (Notification) portal.notifications.get_item (i); - foreach (var action in notification.get_actions ()) { + foreach (var action in notification.list_actions ()) { action_added (action); } } @@ -32,13 +35,18 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { for (uint i = 0; i < portal.notifications.n_items; i++) { var notification = (Notification) portal.notifications.get_item (i); - builder.addv (notification.get_actions ()); + builder.addv (notification.list_actions ()); } return builder.end (); } public void activate_action (string name, Variant? target) { + if (target == null || !target.is_of_type (target_type)) { + warning ("Invalid action target for action %s", name); + return; + } + var parts = name.split ("+", 3); if (parts.length != 3) { @@ -62,8 +70,22 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { switch (type) { case Notification.ACTION_TYPE_ACTION: - var paramters = target != null ? new Variant[] { target } : new Variant[0]; - portal.action_invoked (app_id, id, action_name, paramters); + string activation_token; + Variant action_target; + target.get ("(sv)", out activation_token, out action_target); + + Variant[] action_target_array; + action_target.get ("av", out action_target_array); + + var platform_data = new HashTable (str_hash, str_equal); + platform_data["activation-token"] = activation_token; + + var parameters = new Gee.LinkedList (); + + parameters.add_all_array (action_target_array); + parameters.add (platform_data); + + portal.action_invoked (app_id, id, action_name, parameters.to_array ()); break; case Notification.ACTION_TYPE_INTERNAL: diff --git a/src/Notification/Notification.vala b/src/Notification/Notification.vala index b99f9ec7..2f2e2473 100644 --- a/src/Notification/Notification.vala +++ b/src/Notification/Notification.vala @@ -21,7 +21,7 @@ public class Notification.Notification : GLib.Object { public struct Button { public string label; public string action_name; - public Variant[] action_target; + public Variant action_target; public Button (string internal_id, HashTable data) { if ("label" in data) { @@ -33,9 +33,9 @@ public class Notification.Notification : GLib.Object { } if ("action-target" in data) { - action_target = { data["action-target"] }; + action_target = new Variant[] { data["action-target"] }; } else { - action_target = {}; + action_target = new Variant[] {}; } } } @@ -46,7 +46,7 @@ public class Notification.Notification : GLib.Object { public string app_id; public string dismiss_action_name; public string default_action_name; - public Variant[] default_action_target; + public Variant default_action_target; public Button[] buttons; public DisplayHint display_hint; @@ -60,13 +60,13 @@ public class Notification.Notification : GLib.Object { default_action_name = ACTION_FORMAT.printf (internal_id, raw_data["default-action"].get_string ()); if ("default-action-target" in raw_data) { - default_action_target = { raw_data["default-action-target"] }; + default_action_target = new Variant[] { raw_data["default-action-target"] }; } else { - default_action_target = {}; + default_action_target = new Variant[] {}; } } else { default_action_name = INTERNAL_ACTION_FORMAT.printf (internal_id, "default"); - default_action_target = {}; + default_action_target = new Variant[] {}; } if ("buttons" in raw_data) { @@ -114,7 +114,7 @@ public class Notification.Notification : GLib.Object { notifications_by_internal_id.remove (internal_id); } - public string[] get_actions () { + public string[] list_actions () { string[] actions = new string[data.buttons.length + 2]; actions[0] = data.dismiss_action_name; @@ -141,19 +141,19 @@ public class Notification.Notification : GLib.Object { state_hint = null; state = null; - if (name == data.dismiss_action_name) { + if (name == _data.dismiss_action_name) { parameter_type = null; return true; } - if (name == data.default_action_name) { - parameter_type = variant_type_from_maybe_array (data.default_action_target); + if (name == _data.default_action_name) { + parameter_type = ActionGroup.target_type; return true; } - foreach (var button in data.buttons) { + foreach (var button in _data.buttons) { if (button.action_name == name) { - parameter_type = variant_type_from_maybe_array (button.action_target); + parameter_type = ActionGroup.target_type; return true; } } @@ -161,14 +161,6 @@ public class Notification.Notification : GLib.Object { return false; } - private unowned VariantType? variant_type_from_maybe_array (Variant[] arr) { - if (arr.length == 0) { - return null; - } else { - return arr[0].get_type (); - } - } - public void replace_timestamp (Notification old_notification) { _data.timestamp = old_notification._data.timestamp; } From 14a76e6c3b3fb0a6aad02fad4842b49de18bd468 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Thu, 17 Apr 2025 13:28:29 +0200 Subject: [PATCH 16/16] Fix dismissing --- src/Notification/ActionGroup.vala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Notification/ActionGroup.vala b/src/Notification/ActionGroup.vala index 439ff320..01ae2ba7 100644 --- a/src/Notification/ActionGroup.vala +++ b/src/Notification/ActionGroup.vala @@ -42,11 +42,6 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { } public void activate_action (string name, Variant? target) { - if (target == null || !target.is_of_type (target_type)) { - warning ("Invalid action target for action %s", name); - return; - } - var parts = name.split ("+", 3); if (parts.length != 3) { @@ -70,6 +65,11 @@ public class Notification.ActionGroup : Object, GLib.ActionGroup { switch (type) { case Notification.ACTION_TYPE_ACTION: + if (target == null || !target.is_of_type (target_type)) { + warning ("Invalid action target for action %s", name); + return; + } + string activation_token; Variant action_target; target.get ("(sv)", out activation_token, out action_target);