Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ executable(
'src/Bubble.vala',
'src/Confirmation.vala',
'src/DBus.vala',
'src/FdoActionGroup.vala',
'src/Notification.vala',
'src/Widgets/MaskedImage.vala',
css_gresource,
Expand All @@ -30,6 +31,7 @@ executable(
dependency ('libcanberra-gtk3'),
dependency ('glib-2.0'),
dependency ('gobject-2.0'),
dependency ('gio-2.0'),
dependency ('granite', version: '>=5.4.0'),
dependency ('gtk+-3.0'),
dependency ('libhandy-1')
Expand Down
8 changes: 3 additions & 5 deletions src/Application.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019-2022 elementary, Inc. (https://elementary.io)
* Copyright 2019-2023 elementary, Inc. (https://elementary.io)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
Expand Down Expand Up @@ -30,12 +30,10 @@ public class Notifications.Application : Gtk.Application {
}

protected override bool dbus_register (DBusConnection connection, string object_path) throws Error {
var server = new Notifications.Server ();

try {
connection.register_object ("/org/freedesktop/Notifications", server);
new Notifications.Server (connection);
} catch (Error e) {
warning ("Registring notification server failed: %s", e.message);
Error.prefix_literal (out e, "Registring notification server failed: ");
throw e;
}

Expand Down
91 changes: 50 additions & 41 deletions src/Bubble.vala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/

public class Notifications.Bubble : AbstractBubble {
public signal void action_invoked (string action_key);
public class Notifications.Bubble : AbstractBubble, Gtk.Actionable {
public new string action_name {
get { return get_action_name (); }
set { set_action_name (value); }
}

public new Variant action_target {
get { return get_action_target_value (); }
set { set_action_target_value (value); }
}

public Notification notification {
get {
Expand All @@ -15,15 +23,7 @@ public class Notifications.Bubble : AbstractBubble {
_notification = value;
timeout = 0;

for (int i = 0; i < notification.actions.length; i += 2) {
if (notification.actions[i] == "default") {
_has_default = true;
break;
}
}

var contents = new Contents (value);
contents.action_invoked.connect ((a) => action_invoked (a));
contents.show_all ();

if (value.priority == URGENT) {
Expand All @@ -39,7 +39,6 @@ public class Notifications.Bubble : AbstractBubble {

private Notification _notification;
private Gtk.GestureMultiPress press_gesture;
private bool _has_default;

public Bubble (Notification notification) {
Object (notification: notification);
Expand All @@ -50,30 +49,54 @@ public class Notifications.Bubble : AbstractBubble {
propagation_phase = BUBBLE
};
press_gesture.released.connect (released);

action_invoked.connect (close);
}

private void released () {
if (_has_default) {
action_invoked ("default");
} else if (notification.app_info != null) {
if (action_name != null) {
foreach (unowned var prefix in list_action_prefixes ()) {
if (!action_name.has_prefix (prefix)) {
continue;
}

get_action_group (prefix).activate_action (action_name[prefix.length + 1:], action_target);
press_gesture.set_state (CLAIMED);
return;
}

warning ("cannot activate action '%s': no action group match prefix.", action_name);
}

if (notification.app_info != null) {
notification.app_info.launch_uris_async.begin (null, null, null, (obj, res) => {
try {
((AppInfo) obj).launch_uris_async.end (res);
close ();
closed (Server.CloseReason.UNDEFINED);
} catch (Error e) {
critical ("Unable to launch app: %s", e.message);
warning ("Unable to launch app: %s", e.message);
}
});
}

press_gesture.set_state (CLAIMED);
}

private class Contents : Gtk.Grid {
public signal void action_invoked (string action_key);
// Gtk.Actionable impl
public unowned string? get_action_name () {
return notification.default_action_name;
}

public unowned Variant get_action_target_value () {
return notification.default_action_target;
}

// we ignore the set methods because we query the notification model instead.
public void set_action_name (string? @value) {
}

public void set_action_target_value (Variant? @value) {
}

private class Contents : Gtk.Grid {
public Notifications.Notification notification { get; construct; }

public Contents (Notifications.Notification notification) {
Expand Down Expand Up @@ -145,34 +168,20 @@ public class Notifications.Bubble : AbstractBubble {
attach (title_label, 1, 0);
attach (body_label, 1, 1);

if (notification.actions.length > 0) {
if (notification.buttons.length > 0) {
var action_area = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0) {
halign = Gtk.Align.END,
homogeneous = true
};
action_area.get_style_context ().add_class ("buttonbox");

bool action_area_packed = false;

for (int i = 0; i < notification.actions.length; i += 2) {
if (notification.actions[i] != "default") {
var button = new Gtk.Button.with_label (notification.actions[i + 1]);
var action = notification.actions[i].dup ();

button.clicked.connect (() => {
action_invoked (action);
});

action_area.pack_end (button);

if (!action_area_packed) {
attach (action_area, 0, 2, 2);
action_area_packed = true;
}
} else {
i += 2;
}
foreach (var button in notification.buttons) {
action_area.pack_end (new Gtk.Button.with_label (button.label) {
action_name = button.action_name
});
}

attach (action_area, 0, 2, 2);
}
}
}
Expand Down
93 changes: 65 additions & 28 deletions src/DBus.vala
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,52 @@ public class Notifications.Server : Object {
private const string X_CANONICAL_PRIVATE_SYNCHRONOUS = "x-canonical-private-synchronous";

private uint32 id_counter = 0;
private Notifications.Confirmation? confirmation = null;

private GLib.Settings settings;
private unowned DBusConnection connection;
private Fdo.ActionGroup action_group;

private Gee.HashMap<uint32, Notifications.Bubble> bubbles;
private Gee.Map<uint32, Bubble> bubbles;
private Confirmation? confirmation;

construct {
settings = new GLib.Settings ("io.elementary.notifications");
bubbles = new Gee.HashMap<uint32, Notifications.Bubble> ();
private Settings settings;

private uint action_group_id;
private uint server_id;

public Server (DBusConnection connection) throws Error {
settings = new Settings ("io.elementary.notifications");
bubbles = new Gee.HashMap<uint32, Bubble> ();
action_group = new Fdo.ActionGroup (this);

server_id = connection.register_object ("/org/freedesktop/Notifications", this);
action_group_id = connection.export_action_group ("/org/freedesktop/Notifications", action_group);
this.connection = connection;

action_invoked.connect ((id) => close_bubble (id));
notification_closed.connect ((id) => close_bubble (id));
}

~Server () {
connection.unexport_action_group (action_group_id);
connection.unregister_object (server_id);
}

private void close_bubble (uint32 id) {
Bubble bubble;

if (bubbles.unset (id, out bubble)) {
action_group.remove_actions (id);
bubble.close ();
}
}

public void close_notification (uint32 id) throws DBusError, IOError {
if (bubbles.has_key (id)) {
bubbles[id].close ();
closed_callback (id, CloseReason.CLOSE_NOTIFICATION_CALL);
return;
if (!bubbles.has_key (id)) {
// according to spec, an empty dbus error should be sent if the notification doesn't exist (anymore)
throw new DBusError.FAILED ("");
}

// according to spec, an empty dbus error should be sent if the notification
// doesn't exist (anymore)
throw new DBusError.FAILED ("");
notification_closed (id, CloseReason.CLOSE_NOTIFICATION_CALL);
}

public string [] get_capabilities () throws DBusError, IOError {
Expand Down Expand Up @@ -92,26 +117,43 @@ public class Notifications.Server : Object {
if (hints.contains (X_CANONICAL_PRIVATE_SYNCHRONOUS)) {
send_confirmation (app_icon, hints);
} else {
var notification = new Notifications.Notification (app_name, app_icon, summary, body, actions, hints);
var notification = new Notification (app_name, app_icon, summary, body, hints);
notification.buttons = new GenericArray<Notification.Button?> (actions.length / 2);

// validate actions
for (var i = 0; i < actions.length; i += 2) {
if (actions[i] == "") {
continue;
}

var action_name = "fdo." + action_group.add_action (id, actions[i]);
if (actions[i] == "default") {
notification.default_action_name = action_name;
continue;
}

var label = actions[i + 1].strip ();
if (label == "") {
warning ("action '%s' sent without a label, skipping…", actions[i]);
continue;
}

notification.buttons.add ({ label, action_name });
}

if (!settings.get_boolean ("do-not-disturb") || notification.priority == GLib.NotificationPriority.URGENT) {
var app_settings = new Settings.with_path (
"io.elementary.notifications.applications",
settings.path.concat ("applications", "/", notification.app_id, "/")
);

if (app_settings.get_boolean ("bubbles")) {
if (bubbles.has_key (id) && bubbles[id] != null) {
if (bubbles.has_key (id)) {
bubbles[id].notification = notification;
} else {
bubbles[id] = new Bubble (notification);

bubbles[id].action_invoked.connect ((action_key) => {
action_invoked (id, action_key);
});

bubbles[id].closed.connect ((reason) => {
closed_callback (id, reason);
});
bubbles[id].insert_action_group ("fdo", action_group);
bubbles[id].closed.connect ((res) => notification_closed (id, res));
}

bubbles[id].present ();
Expand All @@ -131,11 +173,6 @@ public class Notifications.Server : Object {
return id;
}

private void closed_callback (uint32 id, uint32 reason) {
bubbles.unset (id);
notification_closed (id, reason);
}

private void send_confirmation (string icon_name, HashTable<string, Variant> hints) {
double progress_value;
Variant? val = hints.lookup ("value");
Expand Down
Loading