diff --git a/src/raven/notifications_view.vala b/src/raven/notifications_view.vala index 796242d..93d1eea 100644 --- a/src/raven/notifications_view.vala +++ b/src/raven/notifications_view.vala @@ -1,705 +1,705 @@ /* * This file is part of budgie-desktop * * Copyright (C) 2015-2016 Ikey Doherty * Copyright 2014 Josh Klar (original Budgie work, prior to Budgie 10) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. */ namespace Budgie { /** Spam apps */ public static const string ROOT_KEY_SPAM_APPS = "spam-apps"; /** Spam categories */ public static const string ROOT_KEY_SPAM_CATEGORIES = "spam-categories"; public enum NotificationCloseReason { EXPIRED = 1, /** The notification expired. */ DISMISSED = 2, /** The notification was dismissed by the user. */ CLOSED = 3, /** The notification was closed by a call to CloseNotification. */ UNDEFINED = 4 /** Undefined/reserved reasons. */ } /** * We only want to make the input safe, we still need actual markup * support, so markup_escape won't be useful here. */ public static string safe_markup_string(string inp) { /* Explicit copy */ string inp2 = "" + inp; /* is it markup? */ if (!(("<" in inp2) && (">" in inp2))) { return Markup.escape_text(inp2); } /* Ensure it's now sane */ if (!("&" in inp2)) { inp2 = inp2.replace("&", "&"); } inp2 = inp2.replace("'", "'"); inp2 = inp2.replace("\"", """); try { if (Pango.parse_markup(inp2, -1, 0, null, null, null)) { return inp2; } } catch (Error e) {} return Markup.escape_text(inp2); } /** * Simple placeholder to use when there are no notifications */ public class NotificationPlaceholder : Gtk.Box { public NotificationPlaceholder() { Object(spacing: 6, orientation: Gtk.Orientation.VERTICAL); get_style_context().add_class("dim-label"); var image = new Gtk.Image.from_icon_name("notification-alert-symbolic", Gtk.IconSize.DIALOG); image.pixel_size = 64; pack_start(image, false, false, 6); var label = new Gtk.Label("%s".printf(_("Nothing to see here"))); label.use_markup = true; pack_start(label, false, false, 0); halign = Gtk.Align.CENTER; valign = Gtk.Align.CENTER; this.show_all(); } } [GtkTemplate (ui = "/com/solus-project/budgie/raven/notification_clone.ui")] public class NotificationClone : Gtk.Grid { [GtkChild] private Gtk.Image? image_icon = null; [GtkChild] private Gtk.Label? label_title = null; [GtkChild] private Gtk.Label? label_body = null; [GtkChild] private Gtk.Label? label_timestamp = null; public NotificationClone(NotificationWindow? target) { if (target.pixbuf != null) { Gdk.Pixbuf scaled_pixbuf = target.pixbuf.scale_simple(32, 32, Gdk.InterpType.NEAREST); this.image_icon.set_from_pixbuf(scaled_pixbuf); } else { this.image_icon.set_from_icon_name(target.icon_name, Gtk.IconSize.INVALID); this.image_icon.pixel_size = 32; } label_title.set_markup(safe_markup_string(target.title)); label_body.set_markup(safe_markup_string(target.body)); var date = new DateTime.from_unix_local(target.timestamp); var gnome_settings = new Settings("org.gnome.desktop.interface"); string clock_format = gnome_settings.get_string("clock-format"); clock_format = (clock_format == "12h") ? date.format("%l:%M %p") : date.format("%H:%M"); label_timestamp.set_text(clock_format); } } [GtkTemplate (ui = "/com/solus-project/budgie/raven/notification.ui")] public class NotificationWindow : Gtk.Window { public NotificationsView? owner { public set ; public get; } public NotificationWindow(NotificationsView? owner) { Object(type: Gtk.WindowType.POPUP, type_hint: Gdk.WindowTypeHint.NOTIFICATION, owner: owner); resizable = false; skip_pager_hint = true; skip_taskbar_hint = true; set_decorated(false); Gdk.Visual? vis = screen.get_rgba_visual(); if (vis != null) { this.set_visual(vis); } cancel = new GLib.Cancellable(); set_default_size(NOTIFICATION_SIZE, -1); button_release_event.connect(()=> { if (!this.has_default_action) { return Gdk.EVENT_PROPAGATE; } did_interact = true; owner.ActionInvoked(this.id, "default"); return Gdk.EVENT_STOP; }); } void action_handler(Gtk.Button? button) { string? action_id = button.get_data("action_id"); if (action_id == null) { return; } did_interact = true; owner.ActionInvoked(this.id, action_id); } public uint32 id; [GtkChild] private Gtk.Image? image_icon = null; [GtkChild] private Gtk.Label? label_title = null; [GtkChild] private Gtk.Label? label_body = null; [GtkChild] private Gtk.Button? button_close = null; [GtkChild] private Gtk.ButtonBox? box_actions = null; public string? title; public string? body; public Gdk.Pixbuf? pixbuf = null; public int64 timestamp; public string app_name; [GtkCallback] void close_clicked() { this.Closed(NotificationCloseReason.DISMISSED); } public signal void Closed(NotificationCloseReason reason); /* Allow deprecated usage */ private string[] img_search = { "image-path", "image_path" }; private string[]? actions = null; HashTable? hints = null; private string? image_path = null; private uint expire_id = 0; private uint32 timeout = 0; private GLib.Cancellable? cancel; public string? category = null; public bool did_interact = false; private bool has_default_action = false; private async bool set_from_image_path(string? app_icon) { if (this.cancel.is_cancelled()) { return false; } /* Update the icon. */ string? img_path = null; foreach (var img in img_search) { var vimg_path = hints.lookup(img); if (vimg_path != null) { img_path = vimg_path.get_string(); break; } } /* This is so fucking unbelievably wrong I can't believe I'm * supporting this abuse. */ if (app_icon != null && "/" in app_icon) { img_path = app_icon; } /* Take the img_path */ if (img_path == null) { return false; } /* Don't unnecessarily update the image */ if (img_path == this.image_path) { return true; } this.image_path = img_path; try { var file = File.new_for_path(image_path); var ins = yield file.read_async(Priority.DEFAULT, null); Gdk.Pixbuf? pbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(ins, 48, 48, true, cancel); this.pixbuf = pbuf; image_icon.set_from_pixbuf(pbuf); } catch (Error e) { return false; } return true; } bool do_expire() { this.Closed(NotificationCloseReason.EXPIRED); return false; } public async void set_from_notify(uint32 id, string app_name, string app_icon, string summary, string body, HashTable hints, int32 expire_timeout) { this.id = id; this.hints = hints; stop_decay(); if (!this.cancel.is_cancelled()) { this.cancel.cancel(); } this.cancel.reset(); var datetime = new DateTime.now_local(); this.timestamp = datetime.to_unix(); bool is_img = yield this.set_from_image_path(app_icon); bool has_desktop = false; if ("desktop-entry" in hints) { this.app_name = hints.lookup("desktop-entry").get_string(); has_desktop = true; } else { this.app_name = app_name; } /* Fallback to named icon if no image-path is specified */ if (!is_img) { this.image_path = null; if (app_icon != "") { image_icon.set_from_icon_name(app_icon, Gtk.IconSize.INVALID); image_icon.pixel_size = 48; this.icon_name = app_icon; } else { /* Use the .desktop icon if we can */ if (has_desktop) { try { string? did = this.app_name; if (!did.has_suffix(".desktop")) { did = "%s.desktop".printf(did); } var app_info = new DesktopAppInfo(did); image_icon.set_from_gicon(app_info.get_icon(), Gtk.IconSize.INVALID); } catch (Error e) { image_icon.set_from_icon_name("mail-unread-symbolic", Gtk.IconSize.INVALID); this.icon_name = "mail-unread-symbolic"; } } else { image_icon.set_from_icon_name("mail-unread-symbolic", Gtk.IconSize.INVALID); this.icon_name = "mail-unread-symbolic"; } image_icon.pixel_size = 48; } } if ("category" in hints) { this.category = hints.lookup("category").get_string(); } if (summary == "") { label_title.set_markup(safe_markup_string(app_name)); this.title = app_name; } else { label_title.set_markup(safe_markup_string(summary)); this.title = summary; } label_body.set_markup(safe_markup_string(body)); this.body = body; this.timeout = expire_timeout; } public void set_actions(string[] actions) { if (this.actions == actions) { return; } if (actions.length == this.actions.length) { bool same = true; for (int i = 0; i < actions.length; i++) { if (actions[i] != this.actions[i]) { same = false; break; } } if (same) { return; } } this.actions = actions; bool icons = hints.contains("action-icons"); if (actions == null || actions.length == 0) { return; } if (actions.length % 2 != 0) { return; } foreach (var kid in box_actions.get_children()) { ulong con_id = kid.get_data("action_con"); SignalHandler.disconnect(kid, con_id); kid.destroy(); } for (int i = 0; i < actions.length; i++) { Gtk.Button? button = null; string action = actions[i]; string local = actions[++i]; if (action == "default" && local == "") { this.has_default_action = true; continue; } if (icons) { if (!action.has_suffix("-symbolic")) { button = new Gtk.Button.from_icon_name("%s-symbolic".printf(action), Gtk.IconSize.MENU); } else { button = new Gtk.Button.from_icon_name(action, Gtk.IconSize.MENU); } /* set action; */ } else { button = new Gtk.Button.with_label(local); button.set_can_focus(false); button.set_can_default(false); } ulong con_id = button.clicked.connect(action_handler); button.set_data("action_con", con_id); button.set_data("action_id", action); box_actions.add(button); } box_actions.show_all(); queue_draw(); } public void begin_decay() { expire_id = Timeout.add(timeout, do_expire); } public void stop_decay() { if (expire_id > 0) { Source.remove(expire_id); expire_id = 0; } } public override void get_preferred_width(out int min, out int nat) { min = nat = NOTIFICATION_SIZE; } public override void get_preferred_width_for_height(int h, out int min, out int nat) { min = nat = NOTIFICATION_SIZE; } } public static const int BUFFER_ZONE = 10; public static const int INITIAL_BUFFER_ZONE = 45; public static const int NOTIFICATION_SIZE = 400; [DBus (name = "org.freedesktop.Notifications")] public class NotificationsView : Gtk.Box { string[] caps = { "body", "body-markup", "actions", "action-icons" }; private Settings settings = new GLib.Settings("com.solus-project.budgie-panel"); private HeaderWidget? header = null; private Gtk.ListBox? listbox; - private GLib.Queue queue = null; + private GLib.Queue stack = null; /* Obviously we'll change this.. */ private HashTable notifications; public async string[] get_capabilities() { return caps; } public void CloseNotification(uint32 id) { if (remove_notification(id)) { this.NotificationClosed(id, NotificationCloseReason.CLOSED); } } private uint32 notif_id = 0; [DBus (visible = false)] void on_notification_closed(NotificationWindow? widget, NotificationCloseReason reason) { ulong nid = widget.get_data("npack_id"); SignalHandler.disconnect(widget, nid); this.NotificationClosed(widget.id, reason); string[] spam_apps = settings.get_strv(Budgie.ROOT_KEY_SPAM_APPS); string[] spam_categories = settings.get_strv(Budgie.ROOT_KEY_SPAM_CATEGORIES); if (reason == NotificationCloseReason.EXPIRED) { if (!(widget.category != null && widget.category in spam_categories) && !(widget.app_name != null && widget.app_name in spam_apps) && !widget.did_interact) { var clone = new NotificationClone(widget); clone.show_all(); this.listbox.add(clone); clone.show_all(); Raven.get_instance().UnreadNotifications(); } } update_child_count(); this.remove_notification(widget.id); } [DBus (visible = false)] bool remove_notification(uint32 id) { unowned NotificationWindow? widget = notifications.lookup(id); if (widget == null) { return false; } widget.stop_decay(); notifications.remove(widget.id); - queue.remove(widget); + stack.remove(widget); widget.destroy(); return true; } [DBus (visible = false)] void update_child_count() { uint len = listbox.get_children().length(); string? text = null; if (len > 1) { text = _("%u unread notifications").printf(len); } else if (len == 1) { text = _("1 unread notification"); } else { text = _("No unread notifications"); } Raven.get_instance().set_notification_count(len); header.text = text; } public uint32 Notify(string app_name, uint32 replaces_id, string app_icon, string summary, string body, string[] actions, HashTable hints, int32 expire_timeout) { ++notif_id; unowned NotificationWindow? pack = null; bool configure = false; if (replaces_id > 0) { pack = notifications.lookup(replaces_id); } int32 expire = expire_timeout; /* Prevent pure derpery. */ if (expire_timeout < 4000 || expire_timeout > 20000) { expire = 4000; } if (pack == null) { var npack = new NotificationWindow(this); ulong nid = npack.Closed.connect(on_notification_closed); npack.set_data("npack_id", nid); notifications.insert(notif_id, npack); pack = npack; configure = true; } else { notifications.steal(notif_id); notifications.insert(notif_id, pack); } string[] actions_copy = {}; foreach (var action in actions) { actions_copy += "%s".printf(action); } /* When we yield vala unrefs everything and we get double frees. GG */ pack.set_from_notify.begin(notif_id, app_name, app_icon, summary, body, hints, expire, ()=> { pack.set_actions(actions_copy); if (configure) { configure_window(pack); } else { pack.begin_decay(); } }); return notif_id; } private void configure_window(NotificationWindow? window) { int x = 0; int y = 0; Gdk.Rectangle rect; - unowned NotificationWindow? tail = queue.peek_tail(); + unowned NotificationWindow? tail = stack.peek_tail(); var screen = Gdk.Screen.get_default(); int mon = screen.get_primary_monitor(); screen.get_monitor_geometry(mon, out rect); if (tail != null) { int nx; int ny; tail.get_position(out nx, out ny); x = nx; y = ny + tail.get_child().get_allocated_height() + BUFFER_ZONE; } else { x = (rect.x+rect.width) - NOTIFICATION_SIZE; x -= BUFFER_ZONE; /* Don't touch lip of next desktop */ y = (rect.y) + INITIAL_BUFFER_ZONE; } - queue.push_tail(window); + stack.push_head(window); window.move(x, y); window.show_all(); window.begin_decay(); } /* Let the client know the notification was closed */ public signal void NotificationClosed(uint32 id, uint32 reason); public signal void ActionInvoked(uint32 id, string action_key); public void GetServerInformation(out string name, out string vendor, out string version, out string spec_version) { name = "Raven"; vendor = "Solus Project"; version = Budgie.VERSION; spec_version = "1.2"; } [DBus (visible = false)] void clear_all() { foreach (var child in listbox.get_children()) { child.destroy(); } update_child_count(); Raven.get_instance().ReadNotifications(); } [DBus (visible = false)] public NotificationsView() { Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0); var img = new Gtk.Image.from_icon_name("list-remove-all-symbolic", Gtk.IconSize.MENU); img.margin_top = 4; var btn = new Gtk.Button.from_icon_name("list-remove-all-symbolic", Gtk.IconSize.MENU); btn.relief = Gtk.ReliefStyle.NONE; header = new HeaderWidget(_("No new notifications"), "notification-alert-symbolic", false, null, btn); header.margin_top = 6; btn.clicked.connect(this.clear_all); pack_start(header, false, false, 0); notifications = new HashTable(direct_hash, direct_equal); - queue = new GLib.Queue(); + stack = new GLib.Queue(); var scrolledwindow = new Gtk.ScrolledWindow(null, null); scrolledwindow.get_style_context().add_class("raven-background"); scrolledwindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); pack_start(scrolledwindow, true, true, 0); listbox = new Gtk.ListBox(); var placeholder = new NotificationPlaceholder(); listbox.set_placeholder(placeholder); scrolledwindow.add(listbox); show_all(); update_child_count(); serve_dbus(); } [DBus (visible = false)] void on_bus_acquired(DBusConnection conn) { try { conn.register_object("/org/freedesktop/Notifications", this); } catch (Error e) { warning("Unable to register notification dbus: %s", e.message); } } [DBus (visible = false)] void serve_dbus() { Bus.own_name(BusType.SESSION, "org.freedesktop.Notifications", BusNameOwnerFlags.NONE, on_bus_acquired, null, null); } } } /* End namespace */ /* * Editor modelines - https://www.wireshark.org/tools/modelines.html * * Local variables: * c-basic-offset: 4 * tab-width: 4 * indent-tabs-mode: nil * End: * * vi: set shiftwidth=4 tabstop=4 expandtab: * :indentSize=4:tabSize=4:noTabs=true: */