// // Copyright (C) 2014 Tom Beckmann // // 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 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // using Meta; namespace Gala.Plugins.Notify { public enum NotificationUrgency { LOW = 0, NORMAL = 1, CRITICAL = 2 } [DBus (name = "org.freedesktop.DBus")] private interface DBus : Object { [DBus (name = "GetConnectionUnixProcessID")] public abstract uint32 get_connection_unix_process_id (string name) throws Error; } [DBus (name = "org.freedesktop.Notifications")] public class NotifyServer : Object { const int DEFAULT_TMEOUT = 4000; const string FALLBACK_ICON = "dialog-information"; [DBus (visible = false)] public signal void show_notification (Notification notification); [DBus (visible = false)] public signal void notification_closed (uint32 id); [DBus (visible = false)] public NotificationStack stack { get; construct; } uint32 id_counter = 0; DBus? bus_proxy = null; public NotifyServer (NotificationStack stack) { Object (stack: stack); } construct { try { bus_proxy = Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.DBus", "/"); } catch (Error e) { warning (e.message); bus_proxy = null; } } public void close_notification (uint32 id) { notification_closed (id); } public string [] get_capabilities () { return { "body", "body-markup", "x-canonical-private-synchronous", "x-canonical-private-icon-only" }; } public void get_server_information (out string name, out string vendor, out string version, out string spec_version) { name = "pantheon-notify"; vendor = "elementaryOS"; version = "0.1"; spec_version = "1.1"; } public new uint32 notify (string app_name, uint32 replaces_id, string app_icon, string summary, string body, string[] actions, HashTable hints, int32 expire_timeout, BusName sender) { var id = replaces_id != 0 ? replaces_id : ++id_counter; var pixbuf = get_pixbuf (hints, app_name, app_icon); var timeout = expire_timeout == uint32.MAX ? DEFAULT_TMEOUT : expire_timeout; var urgency = hints.contains ("urgency") ? (NotificationUrgency) hints.lookup ("urgency").get_byte () : NotificationUrgency.NORMAL; var icon_only = hints.contains ("x-canonical-private-icon-only"); var confirmation = hints.contains ("x-canonical-private-synchronous"); var progress = confirmation && hints.contains ("value"); #if true //debug notifications print ("Notification from '%s', replaces: %u\n" + "\tapp icon: '%s'\n\tsummary: '%s'\n\tbody: '%s'\n\tn actions: %u\n\texpire: %i\n\tHints:\n", app_name, replaces_id, app_icon, summary, body, actions.length); hints.@foreach ((key, val) => { print ("\t\t%s => %s\n", key, val.is_of_type (VariantType.STRING) ? val.get_string () : "<" + val.get_type ().dup_string () + ">"); }); #endif uint32 pid = 0; try { pid = bus_proxy.get_connection_unix_process_id (sender); } catch (Error e) { warning (e.message); } foreach (var child in stack.get_children ()) { unowned Notification notification = (Notification) child; if (notification.being_destroyed) continue; // we only want a single confirmation notification, so we just take the // first one that can be found, no need to check ids or anything var confirmation_notification = notification as ConfirmationNotification; if (confirmation && confirmation_notification != null) { confirmation_notification.update (pixbuf, progress ? hints.@get ("value").get_int32 () : 0, hints.@get ("x-canonical-private-synchronous").get_string (), icon_only, progress); return id; } if (notification.id == id && !notification.being_destroyed) { var normal_notification = notification as NormalNotification; if (normal_notification != null) normal_notification.update (summary, body, pixbuf, expire_timeout, actions); return id; } } Notification notification; if (confirmation) notification = new ConfirmationNotification (id, pixbuf, icon_only, progress ? hints.@get ("value").get_int32 () : -1, hints.@get ("x-canonical-private-synchronous").get_string ()); else notification = new NormalNotification (stack.screen, id, summary, body, pixbuf, urgency, timeout, pid, actions); stack.show_notification (notification); return id; } Gdk.Pixbuf? get_pixbuf (HashTable hints, string app, string icon) { // decide on the icon, order: // - image-data // - image-path // - app_icon // - icon_data // - from app name? // - fallback to dialog-information Gdk.Pixbuf? pixbuf = null; var size = Notification.ICON_SIZE; if (hints.contains ("image_data") || hints.contains ("image-data")) { var data = hints.contains ("image_data") ? hints.lookup ("image_data") : hints.lookup ("image-data"); pixbuf = load_from_variant_at_size (data, size); } else if (hints.contains ("image-path") || hints.contains ("image_path")) { var image_path = (hints.contains ("image-path") ? hints.lookup ("image-path") : hints.lookup ("image_path")).get_string (); try { if (image_path.has_prefix ("file://") || image_path.has_prefix ("/")) { var file_path = File.new_for_commandline_arg (image_path).get_path (); pixbuf = new Gdk.Pixbuf.from_file_at_scale (file_path, size, size, true); } else { pixbuf = Gtk.IconTheme.get_default ().load_icon (image_path, size, 0); } } catch (Error e) { warning (e.message); } } else if (icon != "") { try { var themed = new ThemedIcon.with_default_fallbacks (icon); var info = Gtk.IconTheme.get_default ().lookup_by_gicon (themed, size, 0); if (info != null) pixbuf = info.load_icon (); } catch (Error e) { warning (e.message); } } else if (hints.contains ("icon_data")) { var data = hints.lookup ("icon_data"); pixbuf = load_from_variant_at_size (data, size); } if (pixbuf == null) { try { pixbuf = Gtk.IconTheme.get_default ().load_icon (app.down (), size, 0); } catch (Error e) { try { pixbuf = Gtk.IconTheme.get_default ().load_icon (FALLBACK_ICON, size, 0); } catch (Error e) { warning (e.message); } } } return pixbuf; } static Gdk.Pixbuf? load_from_variant_at_size (Variant variant, int size) { if (!variant.is_of_type (new VariantType ("(iiibiiay)"))) { critical ("notify icon/image-data format invalid"); return null; } int width, height, rowstride, bits_per_sample, n_channels; bool has_alpha; variant.get ("(iiibiiay)", out width, out height, out rowstride, out has_alpha, out bits_per_sample, out n_channels, null); var data = variant.get_child_value (6); unowned uint8[] pixel_data = (uint8[]) data.get_data (); var pixbuf = new Gdk.Pixbuf.with_unowned_data (pixel_data, Gdk.Colorspace.RGB, has_alpha, bits_per_sample, width, height, rowstride, null); return pixbuf.scale_simple (size, size, Gdk.InterpType.BILINEAR); } } }