Merge lp:~gala-dev/gala/notification-plugin into lp:gala

Proposed by Danielle Foré
Status: Merged
Approved by: Danielle Foré
Approved revision: 447
Merged at revision: 405
Proposed branch: lp:~gala-dev/gala/notification-plugin
Merge into: lp:gala
Diff against target: 1683 lines (+1595/-1)
11 files modified
configure.ac (+15/-0)
data/Makefile.am (+2/-1)
data/gala.css (+30/-0)
plugins/Makefile.am (+1/-0)
plugins/notify/ConfirmationNotification.vala (+128/-0)
plugins/notify/Main.vala (+96/-0)
plugins/notify/Makefile.am (+72/-0)
plugins/notify/NormalNotification.vala (+304/-0)
plugins/notify/Notification.vala (+394/-0)
plugins/notify/NotificationStack.vala (+96/-0)
plugins/notify/NotifyServer.vala (+457/-0)
To merge this branch: bzr merge lp:~gala-dev/gala/notification-plugin
Reviewer Review Type Date Requested Status
Danielle Foré Approve
Review via email: mp+227416@code.launchpad.net

Commit message

Implement notifications plugin

To post a comment you must log in.
Revision history for this message
Danielle Foré (danrabbit) wrote :

Icons are rendered below the text, so the critical notification goes under the text instead of over it

Confirmations should probably replace each other. I don't think there's a need to show volume and brightness key responses at the same time.

Revision history for this message
Danielle Foré (danrabbit) wrote :

When notifications from an app replace each other the animation is different than the one we use for replacing confirmations. They should probably be the same.

Also it doesn't seem like replacing a notification resets the timeout

Revision history for this message
Danielle Foré (danrabbit) wrote :

Let's get rid of the bounce before the notification is dismissed :p

Also let's ditch the sound when adjusting your display/keyboard brightness.

Revision history for this message
Tom Beckmann (tombeckmann) wrote :

Should be ready for review now.

441. By Rico Tzschichholz

notifyserver: Clean ups

442. By Rico Tzschichholz

notifyserver: No need to duplicate code

443. By Rico Tzschichholz

notifyserver: Move category-sound-mapping to its own function

444. By Tom Beckmann

reduce memleaks for valac < 0.26

445. By Tom Beckmann

Merge in trunk

446. By Tom Beckmann

use a gtkstylecontext for rendering the notifications, add a fallback style

447. By Tom Beckmann

grab colors for labels from theme (.label and .title)

Revision history for this message
Danielle Foré (danrabbit) wrote :

Alright I'm gonna merge this because we gotta release the damn beta. We've all been testing and it's stable. We can address outstanding issues in trunk

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configure.ac'
2--- configure.ac 2014-07-30 11:48:13 +0000
3+++ configure.ac 2014-08-09 09:56:08 +0000
4@@ -165,6 +165,20 @@
5 fi
6
7 # -----------------------------------------------------------
8+# Dependencies for Notifications plugin
9+# -----------------------------------------------------------
10+
11+NOTIFICATION_PLUGIN_PKGS="libcanberra \
12+ libcanberra-gtk"
13+
14+NOTIFICATION_PLUGIN_VALA_PKGS="--pkg libcanberra \
15+ --pkg libcanberra-gtk"
16+
17+PKG_CHECK_MODULES(NOTIFICAION_PLUGIN, $NOTIFICATION_PLUGIN_PKGS)
18+
19+AC_SUBST([NOTIFICATION_PLUGIN_VALA_PKGS])
20+
21+# -----------------------------------------------------------
22 # Additional configure flags
23 # -----------------------------------------------------------
24
25@@ -232,6 +246,7 @@
26 data/Makefile
27 vapi/Makefile
28 plugins/Makefile
29+plugins/notify/Makefile
30 plugins/zoom/Makefile
31 po/Makefile.in
32 ])
33
34=== modified file 'data/Makefile.am'
35--- data/Makefile.am 2014-07-18 08:07:12 +0000
36+++ data/Makefile.am 2014-08-09 09:56:08 +0000
37@@ -1,5 +1,5 @@
38 stylesdir = $(pkgdatadir)
39-styles_DATA = texture.png close.png
40+styles_DATA = gala.css texture.png close.png
41
42 applicationsdir = $(datadir)/applications
43 applications_DATA = gala.desktop
44@@ -19,6 +19,7 @@
45 all-local: gschemas.compiled
46
47 EXTRA_DIST = \
48+ gala.css \
49 gala.desktop \
50 texture.png \
51 close.png \
52
53=== added file 'data/gala.css'
54--- data/gala.css 1970-01-01 00:00:00 +0000
55+++ data/gala.css 2014-08-09 09:56:08 +0000
56@@ -0,0 +1,30 @@
57+/*
58+ * Copyright (C) 2014 Gala Developers
59+ *
60+ * This program is free software: you can redistribute it and/or modify
61+ * it under the terms of the GNU General Public License as published by
62+ * the Free Software Foundation, either version 3 of the License, or
63+ * (at your option) any later version.
64+ *
65+ * This program is distributed in the hope that it will be useful,
66+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
67+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
68+ * GNU General Public License for more details.
69+ *
70+ * You should have received a copy of the GNU General Public License
71+ * along with this program. If not, see <http: *www.gnu.org/licenses/>.
72+ *
73+ * Authored by: Tom Beckmann
74+ */
75+
76+.gala-notification {
77+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5);
78+ background-color: rgb(2434, 2434, 2434);
79+ border: 1px solid rgba(0, 0, 0, 0.3);
80+ border-radius: 4px;
81+}
82+
83+.gala-notification .title, .gala-notification .label {
84+ color: #333;
85+}
86+
87
88=== modified file 'plugins/Makefile.am'
89--- plugins/Makefile.am 2014-07-18 07:44:04 +0000
90+++ plugins/Makefile.am 2014-08-09 09:56:08 +0000
91@@ -1,4 +1,5 @@
92 SUBDIRS = \
93+ notify \
94 zoom \
95 $(NULL)
96
97
98=== added directory 'plugins/notify'
99=== added file 'plugins/notify/ConfirmationNotification.vala'
100--- plugins/notify/ConfirmationNotification.vala 1970-01-01 00:00:00 +0000
101+++ plugins/notify/ConfirmationNotification.vala 2014-08-09 09:56:08 +0000
102@@ -0,0 +1,128 @@
103+//
104+// Copyright (C) 2014 Tom Beckmann
105+//
106+// This program is free software: you can redistribute it and/or modify
107+// it under the terms of the GNU General Public License as published by
108+// the Free Software Foundation, either version 3 of the License, or
109+// (at your option) any later version.
110+//
111+// This program is distributed in the hope that it will be useful,
112+// but WITHOUT ANY WARRANTY; without even the implied warranty of
113+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
114+// GNU General Public License for more details.
115+//
116+// You should have received a copy of the GNU General Public License
117+// along with this program. If not, see <http://www.gnu.org/licenses/>.
118+//
119+
120+using Clutter;
121+using Meta;
122+
123+namespace Gala.Plugins.Notify
124+{
125+ public class ConfirmationNotification : Notification
126+ {
127+ const int DURATION = 2000;
128+ const int PROGRESS_HEIGHT = 6;
129+
130+ public bool has_progress { get; private set; }
131+
132+ int _progress;
133+ public int progress {
134+ get {
135+ return _progress;
136+ }
137+ private set {
138+ _progress = value;
139+ content.invalidate ();
140+ }
141+ }
142+
143+ public string confirmation_type { get; private set; }
144+
145+ int old_progress;
146+
147+ public ConfirmationNotification (uint32 id, Gdk.Pixbuf? icon, bool icon_only,
148+ int progress, string confirmation_type)
149+ {
150+ Object (id: id, icon: icon, urgency: NotificationUrgency.LOW, expire_timeout: DURATION);
151+
152+ this.icon_only = icon_only;
153+ this.has_progress = progress > -1;
154+ this.progress = progress;
155+ this.confirmation_type = confirmation_type;
156+ }
157+
158+ public override void update_allocation (out float content_height, AllocationFlags flags)
159+ {
160+ content_height = ICON_SIZE;
161+ }
162+
163+ public override void draw_content (Cairo.Context cr)
164+ {
165+ if (!has_progress)
166+ return;
167+
168+ var x = MARGIN + PADDING + ICON_SIZE + SPACING;
169+ var y = MARGIN + PADDING + (ICON_SIZE - PROGRESS_HEIGHT) / 2;
170+ var width = WIDTH - x - MARGIN;
171+
172+ if (!transitioning)
173+ draw_progress_bar (cr, x, y, width, progress);
174+ else {
175+ Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, MARGIN, MARGIN, WIDTH - MARGIN * 2, ICON_SIZE + PADDING * 2, 4);
176+ cr.clip ();
177+
178+ draw_progress_bar (cr, x, y + animation_slide_y_offset, width, old_progress);
179+ draw_progress_bar (cr, x, y + animation_slide_y_offset - animation_slide_height, width, progress);
180+
181+ cr.reset_clip ();
182+ }
183+ }
184+
185+ void draw_progress_bar (Cairo.Context cr, int x, float y, int width, int progress)
186+ {
187+ var fraction = (int) Math.floor (progress.clamp (0, 100) / 100.0 * width);
188+
189+ Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, x, y, width,
190+ PROGRESS_HEIGHT, PROGRESS_HEIGHT / 2);
191+ cr.set_source_rgb (0.8, 0.8, 0.8);
192+ cr.fill ();
193+
194+ if (progress > 0) {
195+ Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, x, y, fraction,
196+ PROGRESS_HEIGHT, PROGRESS_HEIGHT / 2);
197+ cr.set_source_rgb (0.3, 0.3, 0.3);
198+ cr.fill ();
199+ }
200+ }
201+
202+ protected override void update_slide_animation ()
203+ {
204+ // just trigger the draw function, which will move our progress bar down
205+ content.invalidate ();
206+ }
207+
208+ public void update (Gdk.Pixbuf? icon, int progress, string confirmation_type,
209+ bool icon_only)
210+ {
211+ if (this.confirmation_type != confirmation_type) {
212+ this.confirmation_type = confirmation_type;
213+
214+ old_progress = this.progress;
215+
216+ play_update_transition (ICON_SIZE + PADDING * 2);
217+ }
218+
219+ if (this.icon_only != icon_only) {
220+ this.icon_only = icon_only;
221+ queue_relayout ();
222+ }
223+
224+ this.has_progress = progress > -1;
225+ this.progress = progress;
226+
227+ update_base (icon, DURATION);
228+ }
229+ }
230+}
231
232=== added file 'plugins/notify/Main.vala'
233--- plugins/notify/Main.vala 1970-01-01 00:00:00 +0000
234+++ plugins/notify/Main.vala 2014-08-09 09:56:08 +0000
235@@ -0,0 +1,96 @@
236+//
237+// Copyright (C) 2014 Tom Beckmann
238+//
239+// This program is free software: you can redistribute it and/or modify
240+// it under the terms of the GNU General Public License as published by
241+// the Free Software Foundation, either version 3 of the License, or
242+// (at your option) any later version.
243+//
244+// This program is distributed in the hope that it will be useful,
245+// but WITHOUT ANY WARRANTY; without even the implied warranty of
246+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
247+// GNU General Public License for more details.
248+//
249+// You should have received a copy of the GNU General Public License
250+// along with this program. If not, see <http://www.gnu.org/licenses/>.
251+//
252+
253+using Clutter;
254+using Meta;
255+
256+namespace Gala.Plugins.Notify
257+{
258+ public class Main : Gala.Plugin
259+ {
260+ Gala.WindowManager? wm = null;
261+
262+ NotifyServer server;
263+ NotificationStack stack;
264+
265+ public override void initialize (Gala.WindowManager wm)
266+ {
267+ this.wm = wm;
268+ var screen = wm.get_screen ();
269+
270+ stack = new NotificationStack (wm.get_screen ());
271+ wm.ui_group.add_child (stack);
272+ track_actor (stack);
273+
274+ stack.animations_changed.connect ((running) => {
275+ freeze_track = running;
276+ });
277+
278+ server = new NotifyServer (stack);
279+
280+ update_position ();
281+ screen.monitors_changed.connect (update_position);
282+ screen.workareas_changed.connect (update_position);
283+
284+ Bus.own_name (BusType.SESSION, "org.freedesktop.Notifications", BusNameOwnerFlags.NONE,
285+ (connection) => {
286+ try {
287+ connection.register_object ("/org/freedesktop/Notifications", server);
288+ } catch (Error e) {
289+ warning ("Registring notification server failed: %s", e.message);
290+ destroy ();
291+ }
292+ },
293+ () => {},
294+ (con, name) => {
295+ warning ("Could not aquire bus %s", name);
296+ destroy ();
297+ });
298+ }
299+
300+ void update_position ()
301+ {
302+ var screen = wm.get_screen ();
303+ var primary = screen.get_primary_monitor ();
304+ var area = screen.get_active_workspace ().get_work_area_for_monitor (primary);
305+
306+ stack.x = area.x + area.width - stack.width;
307+ stack.y = area.y;
308+ }
309+
310+ public override void destroy ()
311+ {
312+ if (wm == null)
313+ return;
314+
315+ untrack_actor (stack);
316+ stack.destroy ();
317+ }
318+ }
319+}
320+
321+public Gala.PluginInfo register_plugin ()
322+{
323+ return Gala.PluginInfo () {
324+ name = "Notify",
325+ author = "Gala Developers",
326+ plugin_type = typeof (Gala.Plugins.Notify.Main),
327+ provides = Gala.PluginFunction.ADDITION,
328+ load_priority = Gala.LoadPriority.IMMEDIATE
329+ };
330+}
331+
332
333=== added file 'plugins/notify/Makefile.am'
334--- plugins/notify/Makefile.am 1970-01-01 00:00:00 +0000
335+++ plugins/notify/Makefile.am 2014-08-09 09:56:08 +0000
336@@ -0,0 +1,72 @@
337+include $(top_srcdir)/Makefile.common
338+
339+VAPIDIR = $(top_srcdir)/vapi
340+
341+imagedir = $(pkgdatadir)
342+image_DATA = data/image-mask.png
343+
344+BUILT_SOURCES = libgala_notify_la_vala.stamp
345+
346+libgala_notify_la_LTLIBRARIES = libgala-notify.la
347+
348+libgala_notify_ladir = $(pkglibdir)/plugins
349+
350+libgala_notify_la_LDFLAGS = \
351+ $(PLUGIN_LDFLAGS) \
352+ $(GALA_CORE_LDFLAGS) \
353+ $(NOTIFICATION_PLUGIN_LDFLAGS) \
354+ $(top_builddir)/lib/libgala.la \
355+ $(NULL)
356+
357+libgala_notify_la_CFLAGS = \
358+ $(GALA_CORE_CFLAGS) \
359+ $(NOTIFICATION_PLUGIN_CFLAGS) \
360+ -include config.h \
361+ -w \
362+ -I$(top_builddir)/lib \
363+ $(NULL)
364+
365+libgala_notify_la_VALAFLAGS = \
366+ $(GALA_CORE_VALAFLAGS) \
367+ $(NOTIFICATION_PLUGIN_VALA_PKGS) \
368+ $(top_builddir)/lib/gala.vapi \
369+ --vapidir $(VAPIDIR) \
370+ $(VAPIDIR)/config.vapi \
371+ $(NULL)
372+
373+libgala_notify_la_LIBADD = \
374+ $(GALA_CORE_LIBS) \
375+ $(NOTIFICATION_PLUGIN_LIBS) \
376+ $(NULL)
377+
378+libgala_notify_la_VALASOURCES = \
379+ Main.vala \
380+ ConfirmationNotification.vala \
381+ NormalNotification.vala \
382+ Notification.vala \
383+ NotificationStack.vala \
384+ NotifyServer.vala \
385+ $(NULL)
386+
387+nodist_libgala_notify_la_SOURCES = \
388+ $(BUILT_SOURCES) \
389+ $(libgala_notify_la_VALASOURCES:.vala=.c) \
390+ $(NULL)
391+
392+libgala_notify_la_vala.stamp: $(libgala_notify_la_VALASOURCES)
393+ $(AM_V_VALA)$(VALAC) \
394+ $(libgala_notify_la_VALAFLAGS) \
395+ -C \
396+ $(filter %.vala %.c,$^)
397+ $(AM_V_at)touch $@
398+
399+CLEANFILES = \
400+ $(image_DATA) \
401+ $(nodist_libgala_notify_la_SOURCES) \
402+ $(NULL)
403+
404+EXTRA_DIST = \
405+ $(image_DATA) \
406+ $(libgala_notify_la_VALASOURCES) \
407+ $(NULL)
408+
409
410=== added file 'plugins/notify/NormalNotification.vala'
411--- plugins/notify/NormalNotification.vala 1970-01-01 00:00:00 +0000
412+++ plugins/notify/NormalNotification.vala 2014-08-09 09:56:08 +0000
413@@ -0,0 +1,304 @@
414+//
415+// Copyright (C) 2014 Tom Beckmann
416+//
417+// This program is free software: you can redistribute it and/or modify
418+// it under the terms of the GNU General Public License as published by
419+// the Free Software Foundation, either version 3 of the License, or
420+// (at your option) any later version.
421+//
422+// This program is distributed in the hope that it will be useful,
423+// but WITHOUT ANY WARRANTY; without even the implied warranty of
424+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
425+// GNU General Public License for more details.
426+//
427+// You should have received a copy of the GNU General Public License
428+// along with this program. If not, see <http://www.gnu.org/licenses/>.
429+//
430+
431+using Clutter;
432+using Meta;
433+
434+namespace Gala.Plugins.Notify
435+{
436+ /**
437+ * Wrapper class only containing the summary and body label. Allows us to
438+ * instantiate the content very easily for when we need to slide the old
439+ * and new content down.
440+ */
441+ class NormalNotificationContent : Actor
442+ {
443+ static Regex entity_regex;
444+ static Regex tag_regex;
445+
446+ static construct
447+ {
448+ try {
449+ entity_regex = new Regex ("&(?!amp;|quot;|apos;|lt;|gt;)");
450+ tag_regex = new Regex ("<(?!\\/?[biu]>)");
451+ } catch (Error e) {}
452+ }
453+
454+ const int LABEL_SPACING = 2;
455+
456+ Text summary_label;
457+ Text body_label;
458+
459+ construct
460+ {
461+ summary_label = new Text.with_text (null, "");
462+ summary_label.line_wrap = true;
463+ summary_label.use_markup = true;
464+ summary_label.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
465+
466+ body_label = new Text.with_text (null, "");
467+ body_label.line_wrap = true;
468+ body_label.use_markup = true;
469+ body_label.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
470+
471+ var style_path = new Gtk.WidgetPath ();
472+ style_path.append_type (typeof (Gtk.Window));
473+ style_path.append_type (typeof (Gtk.EventBox));
474+ style_path.iter_add_class (1, "gala-notification");
475+ style_path.append_type (typeof (Gtk.Label));
476+
477+ var label_style_context = new Gtk.StyleContext ();
478+ label_style_context.add_provider (Notification.default_css, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
479+ label_style_context.set_path (style_path);
480+
481+ Gdk.RGBA color;
482+
483+ label_style_context.save ();
484+ label_style_context.add_class ("title");
485+ color = label_style_context.get_color (Gtk.StateFlags.NORMAL);
486+ summary_label.color = {
487+ (uint8) (color.red * 255),
488+ (uint8) (color.green * 255),
489+ (uint8) (color.blue * 255),
490+ (uint8) (color.alpha * 255)
491+ };
492+ label_style_context.restore ();
493+
494+ label_style_context.save ();
495+ label_style_context.add_class ("label");
496+ color = label_style_context.get_color (Gtk.StateFlags.NORMAL);
497+ body_label.color = {
498+ (uint8) (color.red * 255),
499+ (uint8) (color.green * 255),
500+ (uint8) (color.blue * 255),
501+ (uint8) (color.alpha * 255)
502+ };
503+ label_style_context.restore ();
504+
505+ add_child (summary_label);
506+ add_child (body_label);
507+ }
508+
509+ public void set_values (string summary, string body)
510+ {
511+ summary_label.set_markup ("<b>%s</b>".printf (fix_markup (summary)));
512+ body_label.set_markup (fix_markup (body));
513+ }
514+
515+ public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
516+ {
517+ float label_height;
518+ get_allocation_values (null, null, null, null, out label_height, null);
519+
520+ min_height = nat_height = label_height;
521+ }
522+
523+ public override void allocate (ActorBox box, AllocationFlags flags)
524+ {
525+ float label_x, label_width, summary_height, body_height, label_height, label_y;
526+ get_allocation_values (out label_x, out label_width, out summary_height,
527+ out body_height, out label_height, out label_y);
528+
529+ var summary_alloc = ActorBox ();
530+ summary_alloc.set_origin (label_x, label_y);
531+ summary_alloc.set_size (label_width, summary_height);
532+ summary_label.allocate (summary_alloc, flags);
533+
534+ var body_alloc = ActorBox ();
535+ body_alloc.set_origin (label_x, label_y + summary_height + LABEL_SPACING);
536+ body_alloc.set_size (label_width, body_height);
537+ body_label.allocate (body_alloc, flags);
538+
539+ base.allocate (box, flags);
540+ }
541+
542+ void get_allocation_values (out float label_x, out float label_width, out float summary_height,
543+ out float body_height, out float label_height, out float label_y)
544+ {
545+ var height = Notification.ICON_SIZE;
546+
547+ label_x = Notification.MARGIN + Notification.PADDING + height + Notification.SPACING;
548+ label_width = Notification.WIDTH - label_x - Notification.MARGIN - Notification.SPACING;
549+
550+ summary_label.get_preferred_height (label_width, null, out summary_height);
551+ body_label.get_preferred_height (label_width, null, out body_height);
552+
553+ label_height = summary_height + LABEL_SPACING + body_height;
554+ label_y = Notification.MARGIN + Notification.PADDING;
555+ // center
556+ if (label_height < height) {
557+ label_y += (height - (int) label_height) / 2;
558+ label_height = height;
559+ }
560+ }
561+
562+ /**
563+ * Copied from gnome-shell, fixes the mess of markup that is sent to us
564+ */
565+ string fix_markup (string markup)
566+ {
567+ var text = markup;
568+
569+ try {
570+ text = entity_regex.replace (markup, markup.length, 0, "&amp;");
571+ text = tag_regex.replace (text, text.length, 0, "&lt;");
572+ } catch (Error e) {}
573+
574+ return text;
575+ }
576+ }
577+
578+ public class NormalNotification : Notification
579+ {
580+ public string summary { get; construct set; }
581+ public string body { get; construct set; }
582+ public uint32 sender_pid { get; construct; }
583+ public string[] notification_actions { get; construct; }
584+ public Screen screen { get; construct; }
585+
586+ Actor content_container;
587+ NormalNotificationContent notification_content;
588+ NormalNotificationContent? old_notification_content = null;
589+
590+ public NormalNotification (Screen screen, uint32 id, string summary, string body, Gdk.Pixbuf? icon,
591+ NotificationUrgency urgency, int32 expire_timeout, uint32 pid, string[] actions)
592+ {
593+ Object (
594+ id: id,
595+ icon: icon,
596+ urgency: urgency,
597+ expire_timeout: expire_timeout,
598+ screen: screen,
599+ summary: summary,
600+ body: body,
601+ sender_pid: pid,
602+ notification_actions: actions
603+ );
604+ }
605+
606+ construct
607+ {
608+ content_container = new Actor ();
609+
610+ notification_content = new NormalNotificationContent ();
611+ notification_content.set_values (summary, body);
612+
613+ content_container.add_child (notification_content);
614+ add_child (content_container);
615+ }
616+
617+ public void update (string summary, string body, Gdk.Pixbuf? icon, int32 expire_timeout,
618+ string[] actions)
619+ {
620+ var visible_change = this.summary != summary || this.body != body;
621+
622+ if (visible_change) {
623+ if (old_notification_content != null)
624+ old_notification_content.destroy ();
625+
626+ old_notification_content = new NormalNotificationContent ();
627+ old_notification_content.set_values (this.summary, this.body);
628+
629+ content_container.add_child (old_notification_content);
630+
631+ this.summary = summary;
632+ this.body = body;
633+ notification_content.set_values (summary, body);
634+
635+ float content_height, old_content_height;
636+ notification_content.get_preferred_height (0, null, out content_height);
637+ old_notification_content.get_preferred_height (0, null, out old_content_height);
638+
639+ content_height = float.max (content_height, old_content_height);
640+
641+ play_update_transition (content_height + PADDING * 2);
642+
643+ get_transition ("switch").completed.connect (() => {
644+ if (old_notification_content != null)
645+ old_notification_content.destroy ();
646+ old_notification_content = null;
647+ });
648+ }
649+
650+ update_base (icon, expire_timeout);
651+ }
652+
653+ protected override void update_slide_animation ()
654+ {
655+ if (old_notification_content != null)
656+ old_notification_content.y = animation_slide_y_offset;
657+
658+ notification_content.y = animation_slide_y_offset - animation_slide_height;
659+ }
660+
661+ public override void update_allocation (out float content_height, AllocationFlags flags)
662+ {
663+ var box = ActorBox ();
664+ box.set_origin (0, 0);
665+ box.set_size (width, height);
666+
667+ content_container.allocate (box, flags);
668+
669+ // the for_width is not needed in our implementation of get_preferred_height as we
670+ // assume a constant width
671+ notification_content.get_preferred_height (0, null, out content_height);
672+
673+ content_container.set_clip (MARGIN, MARGIN, MARGIN * 2 + WIDTH, content_height + PADDING * 2);
674+ }
675+
676+ public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
677+ {
678+ float content_height;
679+ notification_content.get_preferred_height (for_width, null, out content_height);
680+
681+ min_height = nat_height = content_height + (MARGIN + PADDING) * 2;
682+ }
683+
684+ public override void activate ()
685+ {
686+ var window = get_window ();
687+ if (window != null) {
688+ var workspace = window.get_workspace ();
689+ var time = screen.get_display ().get_current_time ();
690+
691+ if (workspace != screen.get_active_workspace ())
692+ workspace.activate_with_focus (window, time);
693+ else
694+ window.activate (time);
695+ }
696+ }
697+
698+ Window? get_window ()
699+ {
700+ if (sender_pid == 0)
701+ return null;
702+
703+ foreach (var actor in Compositor.get_window_actors (screen)) {
704+ var window = actor.get_meta_window ();
705+
706+ // the windows are sorted by stacking order when returned
707+ // from meta_get_window_actors, so we can just pick the first
708+ // one we find and have a pretty good match
709+ if (window.get_pid () == sender_pid)
710+ return window;
711+ }
712+
713+ return null;
714+ }
715+ }
716+}
717+
718
719=== added file 'plugins/notify/Notification.vala'
720--- plugins/notify/Notification.vala 1970-01-01 00:00:00 +0000
721+++ plugins/notify/Notification.vala 2014-08-09 09:56:08 +0000
722@@ -0,0 +1,394 @@
723+//
724+// Copyright (C) 2014 Tom Beckmann
725+//
726+// This program is free software: you can redistribute it and/or modify
727+// it under the terms of the GNU General Public License as published by
728+// the Free Software Foundation, either version 3 of the License, or
729+// (at your option) any later version.
730+//
731+// This program is distributed in the hope that it will be useful,
732+// but WITHOUT ANY WARRANTY; without even the implied warranty of
733+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
734+// GNU General Public License for more details.
735+//
736+// You should have received a copy of the GNU General Public License
737+// along with this program. If not, see <http://www.gnu.org/licenses/>.
738+//
739+
740+using Clutter;
741+using Meta;
742+
743+namespace Gala.Plugins.Notify
744+{
745+ public abstract class Notification : Actor
746+ {
747+ public static Gtk.CssProvider? default_css = null;
748+
749+ public const int WIDTH = 300;
750+ public const int ICON_SIZE = 48;
751+ public const int MARGIN = 12;
752+
753+ public const int SPACING = 6;
754+ public const int PADDING = 4;
755+
756+ public signal void closed (uint32 id, uint32 reason);
757+
758+ public uint32 id { get; construct; }
759+ public Gdk.Pixbuf? icon { get; construct set; }
760+ public NotificationUrgency urgency { get; construct; }
761+ public int32 expire_timeout { get; construct set; }
762+
763+ public uint64 relevancy_time { get; private set; }
764+ public bool being_destroyed { get; private set; default = false; }
765+
766+ protected bool icon_only { get; protected set; default = false; }
767+ protected GtkClutter.Texture icon_texture { get; private set; }
768+ protected Actor icon_container { get; private set; }
769+
770+ /**
771+ * Whether we're currently sliding content for an update animation
772+ */
773+ protected bool transitioning { get; private set; default = false; }
774+
775+ GtkClutter.Texture close_button;
776+
777+ Gtk.StyleContext style_context;
778+
779+ uint remove_timeout = 0;
780+
781+ // temporary things needed for the slide transition
782+ protected float animation_slide_height { get; private set; }
783+ GtkClutter.Texture old_texture;
784+ float _animation_slide_y_offset = 0.0f;
785+ public float animation_slide_y_offset {
786+ get {
787+ return _animation_slide_y_offset;
788+ }
789+ set {
790+ _animation_slide_y_offset = value;
791+
792+ icon_texture.y = -animation_slide_height + _animation_slide_y_offset;
793+ old_texture.y = _animation_slide_y_offset;
794+
795+ update_slide_animation ();
796+ }
797+ }
798+
799+ public Notification (uint32 id, Gdk.Pixbuf? icon, NotificationUrgency urgency,
800+ int32 expire_timeout)
801+ {
802+ Object (
803+ id: id,
804+ icon: icon,
805+ urgency: urgency,
806+ expire_timeout: expire_timeout
807+ );
808+ }
809+
810+ construct
811+ {
812+ relevancy_time = new DateTime.now_local ().to_unix ();
813+ width = WIDTH + MARGIN * 2;
814+ reactive = true;
815+ set_pivot_point (0.5f, 0.5f);
816+
817+ icon_texture = new GtkClutter.Texture ();
818+ icon_texture.set_pivot_point (0.5f, 0.5f);
819+
820+ icon_container = new Actor ();
821+ icon_container.add_child (icon_texture);
822+
823+ close_button = Utils.create_close_button ();
824+ close_button.opacity = 0;
825+ close_button.reactive = true;
826+ close_button.set_easing_duration (300);
827+
828+ var close_click = new ClickAction ();
829+ close_click.clicked.connect (() => {
830+ closed (id, NotificationClosedReason.DISMISSED);
831+ close ();
832+ });
833+ close_button.add_action (close_click);
834+
835+ add_child (icon_container);
836+ add_child (close_button);
837+
838+ if (default_css == null) {
839+ default_css = new Gtk.CssProvider ();
840+ try {
841+ default_css.load_from_path (Config.PKGDATADIR + "/gala.css");
842+ } catch (Error e) {
843+ warning ("Loading default styles failed: %s", e.message);
844+ }
845+ }
846+
847+ var style_path = new Gtk.WidgetPath ();
848+ style_path.append_type (typeof (Gtk.Window));
849+ style_path.append_type (typeof (Gtk.EventBox));
850+
851+ style_context = new Gtk.StyleContext ();
852+ style_context.add_provider (default_css, Gtk.STYLE_PROVIDER_PRIORITY_FALLBACK);
853+ style_context.add_class ("gala-notification");
854+ style_context.set_path (style_path);
855+
856+ var label_style_path = style_path.copy ();
857+ label_style_path.iter_add_class (1, "gala-notification");
858+ label_style_path.append_type (typeof (Gtk.Label));
859+
860+ var canvas = new Canvas ();
861+ canvas.draw.connect (draw);
862+ content = canvas;
863+
864+ set_values ();
865+
866+ var click = new ClickAction ();
867+ click.clicked.connect (() => {
868+ activate ();
869+ });
870+ add_action (click);
871+
872+ open ();
873+ }
874+
875+ public void open () {
876+ var entry = new TransitionGroup ();
877+ entry.remove_on_complete = true;
878+ entry.duration = 400;
879+
880+ var opacity_transition = new PropertyTransition ("opacity");
881+ opacity_transition.set_from_value (0);
882+ opacity_transition.set_to_value (255);
883+
884+ var flip_transition = new KeyframeTransition ("rotation-angle-x");
885+ flip_transition.set_from_value (90.0);
886+ flip_transition.set_to_value (0.0);
887+ flip_transition.set_key_frames ({ 0.6 });
888+ flip_transition.set_values ({ -10.0 });
889+
890+ entry.add_transition (opacity_transition);
891+ entry.add_transition (flip_transition);
892+ add_transition ("entry", entry);
893+
894+ switch (urgency) {
895+ case NotificationUrgency.LOW:
896+ case NotificationUrgency.NORMAL:
897+ return;
898+ case NotificationUrgency.CRITICAL:
899+ var icon_entry = new TransitionGroup ();
900+ icon_entry.duration = 1000;
901+ icon_entry.remove_on_complete = true;
902+ icon_entry.progress_mode = AnimationMode.EASE_IN_OUT_CUBIC;
903+
904+ double[] keyframes = { 0.2, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
905+ GLib.Value[] scale = { 0.0, 1.2, 1.6, 1.6, 1.6, 1.6, 1.2, 1.0 };
906+
907+ var rotate_transition = new KeyframeTransition ("rotation-angle-z");
908+ rotate_transition.set_from_value (30.0);
909+ rotate_transition.set_to_value (0.0);
910+ rotate_transition.set_key_frames (keyframes);
911+ rotate_transition.set_values ({ 30.0, -30.0, 30.0, -20.0, 10.0, -5.0, 2.0, 0.0 });
912+
913+ var scale_x_transition = new KeyframeTransition ("scale-x");
914+ scale_x_transition.set_from_value (0.0);
915+ scale_x_transition.set_to_value (1.0);
916+ scale_x_transition.set_key_frames (keyframes);
917+ scale_x_transition.set_values (scale);
918+
919+ var scale_y_transition = new KeyframeTransition ("scale-y");
920+ scale_y_transition.set_from_value (0.0);
921+ scale_y_transition.set_to_value (1.0);
922+ scale_y_transition.set_key_frames (keyframes);
923+ scale_y_transition.set_values (scale);
924+
925+ icon_entry.add_transition (rotate_transition);
926+ icon_entry.add_transition (scale_x_transition);
927+ icon_entry.add_transition (scale_y_transition);
928+
929+ icon_texture.add_transition ("entry", icon_entry);
930+ return;
931+ }
932+ }
933+
934+ public void close ()
935+ {
936+ set_easing_duration (100);
937+
938+ set_easing_mode (AnimationMode.EASE_IN_QUAD);
939+ opacity = 0;
940+
941+ x = WIDTH + MARGIN * 2;
942+
943+ being_destroyed = true;
944+ var transition = get_transition ("x");
945+ if (transition != null)
946+ transition.completed.connect (() => destroy ());
947+ else
948+ destroy ();
949+ }
950+
951+ protected void update_base (Gdk.Pixbuf? icon, int32 expire_timeout)
952+ {
953+ this.icon = icon;
954+ this.expire_timeout = expire_timeout;
955+ this.relevancy_time = new DateTime.now_local ().to_unix ();
956+
957+ set_values ();
958+ }
959+
960+ void set_values ()
961+ {
962+ if (icon != null) {
963+ try {
964+ icon_texture.set_from_pixbuf (icon);
965+ } catch (Error e) {}
966+ }
967+
968+ set_timeout ();
969+ }
970+
971+ void set_timeout ()
972+ {
973+ // crtitical notifications have to be dismissed manually
974+ if (expire_timeout <= 0 || urgency == NotificationUrgency.CRITICAL)
975+ return;
976+
977+ clear_timeout ();
978+
979+ remove_timeout = Timeout.add (expire_timeout, () => {
980+ closed (id, NotificationClosedReason.EXPIRED);
981+ close ();
982+ remove_timeout = 0;
983+ return false;
984+ });
985+ }
986+
987+ void clear_timeout ()
988+ {
989+ if (remove_timeout != 0) {
990+ Source.remove (remove_timeout);
991+ remove_timeout = 0;
992+ }
993+ }
994+
995+ public override bool enter_event (CrossingEvent event)
996+ {
997+ close_button.opacity = 255;
998+
999+ clear_timeout ();
1000+
1001+ return true;
1002+ }
1003+
1004+ public override bool leave_event (CrossingEvent event)
1005+ {
1006+ close_button.opacity = 0;
1007+
1008+ // TODO consider decreasing the timeout now or calculating the remaining
1009+ set_timeout ();
1010+
1011+ return true;
1012+ }
1013+
1014+ public virtual void activate ()
1015+ {
1016+ }
1017+
1018+ public virtual void draw_content (Cairo.Context cr)
1019+ {
1020+ }
1021+
1022+ public abstract void update_allocation (out float content_height, AllocationFlags flags);
1023+
1024+ public override void allocate (ActorBox box, AllocationFlags flags)
1025+ {
1026+ var icon_alloc = ActorBox ();
1027+
1028+ icon_alloc.set_origin (icon_only ? (WIDTH - ICON_SIZE) / 2 : MARGIN + PADDING, MARGIN + PADDING);
1029+ icon_alloc.set_size (ICON_SIZE, ICON_SIZE);
1030+ icon_container.allocate (icon_alloc, flags);
1031+
1032+ var close_alloc = ActorBox ();
1033+ close_alloc.set_origin (MARGIN + PADDING - close_button.width / 2,
1034+ MARGIN + PADDING - close_button.height / 2);
1035+ close_alloc.set_size (close_button.width, close_button.height);
1036+ close_button.allocate (close_alloc, flags);
1037+
1038+ float content_height;
1039+ update_allocation (out content_height, flags);
1040+ box.set_size (MARGIN * 2 + WIDTH, (MARGIN + PADDING) * 2 + content_height);
1041+
1042+ base.allocate (box, flags);
1043+
1044+ var canvas = (Canvas) content;
1045+ var canvas_width = (int) box.get_width ();
1046+ var canvas_height = (int) box.get_height ();
1047+ if (canvas.width != canvas_width || canvas.height != canvas_height)
1048+ canvas.set_size (canvas_width, canvas_height);
1049+ }
1050+
1051+ public override void get_preferred_height (float for_width, out float min_height, out float nat_height)
1052+ {
1053+ min_height = nat_height = ICON_SIZE + (MARGIN + PADDING) * 2;
1054+ }
1055+
1056+ protected void play_update_transition (float slide_height)
1057+ {
1058+ Transition transition;
1059+ if ((transition = get_transition ("switch")) != null) {
1060+ transition.completed ();
1061+ remove_transition ("switch");
1062+ }
1063+
1064+ animation_slide_height = slide_height;
1065+
1066+ old_texture = new GtkClutter.Texture ();
1067+ icon_container.add_child (old_texture);
1068+ icon_container.set_clip (0, -PADDING, ICON_SIZE, ICON_SIZE + PADDING * 2);
1069+
1070+ try {
1071+ old_texture.set_from_pixbuf (this.icon);
1072+ } catch (Error e) {}
1073+
1074+ transition = new PropertyTransition ("animation-slide-y-offset");
1075+ transition.duration = 200;
1076+ transition.progress_mode = AnimationMode.EASE_IN_OUT_QUAD;
1077+ transition.set_from_value (0.0f);
1078+ transition.set_to_value (animation_slide_height);
1079+ transition.remove_on_complete = true;
1080+
1081+ transition.completed.connect (() => {
1082+ old_texture.destroy ();
1083+ icon_container.remove_clip ();
1084+ _animation_slide_y_offset = 0;
1085+ transitioning = false;
1086+ });
1087+
1088+ add_transition ("switch", transition);
1089+ transitioning = true;
1090+ }
1091+
1092+ protected virtual void update_slide_animation ()
1093+ {
1094+ }
1095+
1096+ bool draw (Cairo.Context cr)
1097+ {
1098+ var canvas = (Canvas) content;
1099+
1100+ var x = MARGIN;
1101+ var y = MARGIN;
1102+ var width = canvas.width - MARGIN * 2;
1103+ var height = canvas.height - MARGIN * 2;
1104+ cr.set_operator (Cairo.Operator.CLEAR);
1105+ cr.paint ();
1106+ cr.set_operator (Cairo.Operator.OVER);
1107+
1108+ style_context.render_background (cr, x, y, width, height);
1109+ style_context.render_frame (cr, x, y, width, height);
1110+
1111+ draw_content (cr);
1112+
1113+ return false;
1114+ }
1115+ }
1116+}
1117
1118=== added file 'plugins/notify/NotificationStack.vala'
1119--- plugins/notify/NotificationStack.vala 1970-01-01 00:00:00 +0000
1120+++ plugins/notify/NotificationStack.vala 2014-08-09 09:56:08 +0000
1121@@ -0,0 +1,96 @@
1122+//
1123+// Copyright (C) 2014 Tom Beckmann
1124+//
1125+// This program is free software: you can redistribute it and/or modify
1126+// it under the terms of the GNU General Public License as published by
1127+// the Free Software Foundation, either version 3 of the License, or
1128+// (at your option) any later version.
1129+//
1130+// This program is distributed in the hope that it will be useful,
1131+// but WITHOUT ANY WARRANTY; without even the implied warranty of
1132+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1133+// GNU General Public License for more details.
1134+//
1135+// You should have received a copy of the GNU General Public License
1136+// along with this program. If not, see <http://www.gnu.org/licenses/>.
1137+//
1138+
1139+using Clutter;
1140+using Meta;
1141+
1142+namespace Gala.Plugins.Notify
1143+{
1144+ public class NotificationStack : Actor
1145+ {
1146+ const int ADDITIONAL_MARGIN = 12;
1147+
1148+ public signal void animations_changed (bool running);
1149+
1150+ public Screen screen { get; construct; }
1151+
1152+ int animation_counter = 0;
1153+
1154+ public NotificationStack (Screen screen)
1155+ {
1156+ Object (screen: screen);
1157+ }
1158+
1159+ construct
1160+ {
1161+ width = Notification.WIDTH + 2 * Notification.MARGIN + ADDITIONAL_MARGIN;
1162+ clip_to_allocation = true;
1163+ }
1164+
1165+ public void show_notification (Notification notification)
1166+ {
1167+ if (animation_counter == 0)
1168+ animations_changed (true);
1169+
1170+ // raise ourselves when we got something to show
1171+ get_parent ().set_child_above_sibling (this, null);
1172+
1173+ // we have a shoot-over on the start of the close animation, which gets clipped
1174+ // unless we make our container a bit wider and move the notifications over
1175+ notification.margin_left = ADDITIONAL_MARGIN;
1176+
1177+ notification.destroy.connect (() => {
1178+ update_positions ();
1179+ });
1180+
1181+ float height;
1182+ notification.get_preferred_height (Notification.WIDTH, out height, null);
1183+ update_positions (height);
1184+
1185+ insert_child_at_index (notification, 0);
1186+
1187+ animation_counter++;
1188+
1189+ notification.get_transition ("entry").completed.connect (() => {
1190+ if (--animation_counter == 0)
1191+ animations_changed (false);
1192+ });
1193+ }
1194+
1195+ void update_positions (float add_y = 0.0f)
1196+ {
1197+ var y = add_y;
1198+ var i = get_n_children ();
1199+ var delay_step = i > 0 ? 150 / i : 0;
1200+ foreach (var child in get_children ()) {
1201+ if (((Notification) child).being_destroyed)
1202+ continue;
1203+
1204+ child.save_easing_state ();
1205+ child.set_easing_mode (AnimationMode.EASE_OUT_BACK);
1206+ child.set_easing_duration (200);
1207+ child.set_easing_delay ((i--) * delay_step);
1208+
1209+ child.y = y;
1210+ child.restore_easing_state ();
1211+
1212+ y += child.height;
1213+ }
1214+ }
1215+ }
1216+}
1217+
1218
1219=== added file 'plugins/notify/NotifyServer.vala'
1220--- plugins/notify/NotifyServer.vala 1970-01-01 00:00:00 +0000
1221+++ plugins/notify/NotifyServer.vala 2014-08-09 09:56:08 +0000
1222@@ -0,0 +1,457 @@
1223+//
1224+// Copyright (C) 2014 Tom Beckmann
1225+//
1226+// This program is free software: you can redistribute it and/or modify
1227+// it under the terms of the GNU General Public License as published by
1228+// the Free Software Foundation, either version 3 of the License, or
1229+// (at your option) any later version.
1230+//
1231+// This program is distributed in the hope that it will be useful,
1232+// but WITHOUT ANY WARRANTY; without even the implied warranty of
1233+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1234+// GNU General Public License for more details.
1235+//
1236+// You should have received a copy of the GNU General Public License
1237+// along with this program. If not, see <http://www.gnu.org/licenses/>.
1238+//
1239+
1240+using Meta;
1241+
1242+namespace Gala.Plugins.Notify
1243+{
1244+ public enum NotificationUrgency
1245+ {
1246+ LOW = 0,
1247+ NORMAL = 1,
1248+ CRITICAL = 2
1249+ }
1250+
1251+ public enum NotificationClosedReason
1252+ {
1253+ EXPIRED = 1,
1254+ DISMISSED = 2,
1255+ CLOSE_NOTIFICATION_CALL = 3,
1256+ UNDEFINED = 4
1257+ }
1258+
1259+ [DBus (name = "org.freedesktop.DBus")]
1260+ private interface DBus : Object
1261+ {
1262+ [DBus (name = "GetConnectionUnixProcessID")]
1263+ public abstract uint32 get_connection_unix_process_id (string name) throws Error;
1264+ }
1265+
1266+ [DBus (name = "org.freedesktop.Notifications")]
1267+ public class NotifyServer : Object
1268+ {
1269+ const int DEFAULT_TMEOUT = 4000;
1270+ const string FALLBACK_ICON = "dialog-information";
1271+
1272+ [DBus (visible = false)]
1273+ public signal void show_notification (Notification notification);
1274+
1275+ public signal void notification_closed (uint32 id, uint32 reason);
1276+ public signal void action_invoked (uint32 id, string action_key);
1277+
1278+ [DBus (visible = false)]
1279+ public NotificationStack stack { get; construct; }
1280+
1281+ uint32 id_counter = 0;
1282+
1283+ DBus? bus_proxy = null;
1284+ unowned Canberra.Context? ca_context = null;
1285+
1286+ public NotifyServer (NotificationStack stack)
1287+ {
1288+ Object (stack: stack);
1289+ }
1290+
1291+ construct
1292+ {
1293+ try {
1294+ bus_proxy = Bus.get_proxy_sync (BusType.SESSION, "org.freedesktop.DBus", "/");
1295+ } catch (Error e) {
1296+ warning (e.message);
1297+ bus_proxy = null;
1298+ }
1299+
1300+ var locale = Intl.setlocale (LocaleCategory.MESSAGES, null);
1301+ ca_context = CanberraGtk.context_get ();
1302+ ca_context.change_props (Canberra.PROP_APPLICATION_NAME, "Gala",
1303+ Canberra.PROP_APPLICATION_ID, "org.pantheon.gala",
1304+ Canberra.PROP_APPLICATION_NAME, "start-here",
1305+ Canberra.PROP_APPLICATION_LANGUAGE, locale,
1306+ null);
1307+ ca_context.open ();
1308+ }
1309+
1310+ public string [] get_capabilities ()
1311+ {
1312+ return {
1313+ "body",
1314+ "body-markup",
1315+ "sound",
1316+ "x-canonical-private-synchronous",
1317+ "x-canonical-private-icon-only"
1318+ };
1319+ }
1320+
1321+ public void get_server_information (out string name, out string vendor,
1322+ out string version, out string spec_version)
1323+ {
1324+ name = "pantheon-notify";
1325+ vendor = "elementaryOS";
1326+ version = "0.1";
1327+ spec_version = "1.1";
1328+ }
1329+
1330+ /**
1331+ * Implementation of the CloseNotification DBus method
1332+ *
1333+ * @param id The id of the notification to be closed.
1334+ */
1335+ public void close_notification (uint32 id) throws DBusError
1336+ {
1337+ foreach (var child in stack.get_children ()) {
1338+ unowned Notification notification = (Notification) child;
1339+ if (notification.id != id)
1340+ continue;
1341+
1342+ notification_closed_callback (notification, id,
1343+ NotificationClosedReason.CLOSE_NOTIFICATION_CALL);
1344+ notification.close ();
1345+
1346+ return;
1347+ }
1348+
1349+ // according to spec, an empty dbus error should be sent if the notification
1350+ // doesn't exist (anymore)
1351+ throw new DBusError.FAILED ("");
1352+ }
1353+
1354+ public new uint32 notify (string app_name, uint32 replaces_id, string app_icon, string summary,
1355+ string body, string[] actions, HashTable<string, Variant> hints, int32 expire_timeout, BusName sender)
1356+ {
1357+ Variant? variant;
1358+
1359+ var id = replaces_id != 0 ? replaces_id : ++id_counter;
1360+ var pixbuf = get_pixbuf (app_name, app_icon, hints);
1361+ var timeout = expire_timeout == uint32.MAX ? DEFAULT_TMEOUT : expire_timeout;
1362+
1363+ var urgency = NotificationUrgency.NORMAL;
1364+ if ((variant = hints.lookup ("urgency")) != null)
1365+ urgency = (NotificationUrgency) variant.get_byte ();
1366+
1367+ var icon_only = hints.contains ("x-canonical-private-icon-only");
1368+ var confirmation = hints.contains ("x-canonical-private-synchronous");
1369+ var progress = confirmation && hints.contains ("value");
1370+
1371+#if 0 // enable to debug notifications
1372+ print ("Notification from '%s', replaces: %u\n" +
1373+ "\tapp icon: '%s'\n\tsummary: '%s'\n\tbody: '%s'\n\tn actions: %u\n\texpire: %i\n\tHints:\n",
1374+ app_name, replaces_id, app_icon, summary, body, actions.length);
1375+ hints.@foreach ((key, val) => {
1376+ print ("\t\t%s => %s\n", key, val.is_of_type (VariantType.STRING) ?
1377+ val.get_string () : "<" + val.get_type ().dup_string () + ">");
1378+ });
1379+#endif
1380+
1381+ uint32 pid = 0;
1382+ try {
1383+ pid = bus_proxy.get_connection_unix_process_id (sender);
1384+ } catch (Error e) { warning (e.message); }
1385+
1386+ handle_sounds (hints);
1387+
1388+ foreach (var child in stack.get_children ()) {
1389+ unowned Notification notification = (Notification) child;
1390+
1391+ if (notification.being_destroyed)
1392+ continue;
1393+
1394+ // we only want a single confirmation notification, so we just take the
1395+ // first one that can be found, no need to check ids or anything
1396+ unowned ConfirmationNotification? confirmation_notification = notification as ConfirmationNotification;
1397+ if (confirmation
1398+ && confirmation_notification != null) {
1399+ confirmation_notification.update (pixbuf,
1400+ progress ? hints.@get ("value").get_int32 () : -1,
1401+ hints.@get ("x-canonical-private-synchronous").get_string (),
1402+ icon_only);
1403+
1404+ return id;
1405+ }
1406+
1407+ unowned NormalNotification? normal_notification = notification as NormalNotification;
1408+ if (!confirmation
1409+ && notification.id == id
1410+ && normal_notification != null) {
1411+
1412+ normal_notification.update (summary, body, pixbuf, timeout, actions);
1413+
1414+ return id;
1415+ }
1416+ }
1417+
1418+ Notification notification;
1419+ if (confirmation)
1420+ notification = new ConfirmationNotification (id, pixbuf, icon_only,
1421+ progress ? hints.@get ("value").get_int32 () : -1,
1422+ hints.@get ("x-canonical-private-synchronous").get_string ());
1423+ else
1424+ notification = new NormalNotification (stack.screen, id, summary, body, pixbuf,
1425+ urgency, timeout, pid, actions);
1426+
1427+ notification.closed.connect (notification_closed_callback);
1428+ stack.show_notification (notification);
1429+
1430+#if !VALA_0_26
1431+ // fixes memleaks as described in https://bugzilla.gnome.org/show_bug.cgi?id=698260
1432+ // valac >= 0.26 already has this fix
1433+ hints.@foreach ((key, val) => {
1434+ g_variant_unref (val);
1435+ });
1436+#endif
1437+
1438+ return id;
1439+ }
1440+
1441+ static Gdk.Pixbuf? get_pixbuf (string app_name, string app_icon, HashTable<string, Variant> hints)
1442+ {
1443+ // decide on the icon, order:
1444+ // - image-data
1445+ // - image-path
1446+ // - app_icon
1447+ // - icon_data
1448+ // - from app name?
1449+ // - fallback to dialog-information
1450+
1451+ Gdk.Pixbuf? pixbuf = null;
1452+ Variant? variant = null;
1453+ var size = Notification.ICON_SIZE;
1454+ var mask_offset = 4;
1455+ var mask_size_offset = mask_offset * 2;
1456+ var has_mask = false;
1457+
1458+ if ((variant = hints.lookup ("image-data")) != null
1459+ || (variant = hints.lookup ("image_data")) != null
1460+ || (variant = hints.lookup ("icon_data")) != null) {
1461+
1462+ has_mask = true;
1463+ size = size - mask_size_offset;
1464+
1465+ pixbuf = load_from_variant_at_size (variant, size);
1466+
1467+ } else if ((variant = hints.lookup ("image-path")) != null
1468+ || (variant = hints.lookup ("image_path")) != null) {
1469+
1470+ var image_path = variant.get_string ();
1471+
1472+ try {
1473+ if (image_path.has_prefix ("file://") || image_path.has_prefix ("/")) {
1474+ has_mask = true;
1475+ size = size - mask_size_offset;
1476+
1477+ var file_path = File.new_for_commandline_arg (image_path).get_path ();
1478+ pixbuf = new Gdk.Pixbuf.from_file_at_scale (file_path, size, size, true);
1479+ } else {
1480+ pixbuf = Gtk.IconTheme.get_default ().load_icon (image_path, size, 0);
1481+ }
1482+ } catch (Error e) { warning (e.message); }
1483+
1484+ } else if (app_icon != "") {
1485+
1486+ try {
1487+ var themed = new ThemedIcon.with_default_fallbacks (app_icon);
1488+ var info = Gtk.IconTheme.get_default ().lookup_by_gicon (themed, size, 0);
1489+ if (info != null)
1490+ pixbuf = info.load_icon ();
1491+ } catch (Error e) { warning (e.message); }
1492+
1493+ }
1494+
1495+ if (pixbuf == null) {
1496+
1497+ try {
1498+ pixbuf = Gtk.IconTheme.get_default ().load_icon (app_name.down (), size, 0);
1499+ } catch (Error e) {
1500+
1501+ try {
1502+ pixbuf = Gtk.IconTheme.get_default ().load_icon (FALLBACK_ICON, size, 0);
1503+ } catch (Error e) { warning (e.message); }
1504+ }
1505+ } else if (has_mask) {
1506+ var mask_size = Notification.ICON_SIZE;
1507+ var offset_x = mask_offset;
1508+ var offset_y = mask_offset + 1;
1509+
1510+ var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, mask_size, mask_size);
1511+ var cr = new Cairo.Context (surface);
1512+
1513+ Granite.Drawing.Utilities.cairo_rounded_rectangle (cr,
1514+ offset_x, offset_y, size, size, 4);
1515+ cr.clip ();
1516+
1517+ Gdk.cairo_set_source_pixbuf (cr, pixbuf, offset_x, offset_y);
1518+ cr.paint ();
1519+
1520+ cr.reset_clip ();
1521+
1522+ var mask = new Cairo.ImageSurface.from_png (Config.PKGDATADIR + "/image-mask.png");
1523+ cr.set_source_surface (mask, 0, 0);
1524+ cr.paint ();
1525+
1526+ pixbuf = Gdk.pixbuf_get_from_surface (surface, 0, 0, mask_size, mask_size);
1527+ }
1528+
1529+ return pixbuf;
1530+ }
1531+
1532+ static Gdk.Pixbuf? load_from_variant_at_size (Variant variant, int size)
1533+ {
1534+ if (!variant.is_of_type (new VariantType ("(iiibiiay)"))) {
1535+ critical ("notify icon/image-data format invalid");
1536+ return null;
1537+ }
1538+
1539+ int width, height, rowstride, bits_per_sample, n_channels;
1540+ bool has_alpha;
1541+
1542+ variant.get ("(iiibiiay)", out width, out height, out rowstride,
1543+ out has_alpha, out bits_per_sample, out n_channels, null);
1544+
1545+ var data = variant.get_child_value (6);
1546+ unowned uint8[] pixel_data = (uint8[]) data.get_data ();
1547+
1548+ var pixbuf = new Gdk.Pixbuf.with_unowned_data (pixel_data, Gdk.Colorspace.RGB, has_alpha,
1549+ bits_per_sample, width, height, rowstride, null);
1550+
1551+ return pixbuf.scale_simple (size, size, Gdk.InterpType.BILINEAR);
1552+ }
1553+
1554+ void handle_sounds (HashTable<string,Variant> hints)
1555+ {
1556+ if (ca_context == null)
1557+ return;
1558+
1559+ Variant? variant = null;
1560+
1561+ // Are we suppose to play a sound at all?
1562+ if ((variant = hints.lookup ("supress-sound")) != null
1563+ && variant.get_boolean ())
1564+ return;
1565+
1566+ Canberra.Proplist props;
1567+ Canberra.Proplist.create (out props);
1568+ props.sets (Canberra.PROP_CANBERRA_CACHE_CONTROL, "volatile");
1569+
1570+ bool play_sound = false;
1571+
1572+ // no sounds for confirmation bubbles
1573+ if ((variant = hints.lookup ("x-canonical-private-synchronous")) != null) {
1574+ var confirmation_type = variant.get_string ();
1575+
1576+ // the sound indicator is an exception here, it won't emit a sound at all, even though for
1577+ // consistency it should. So we make it emit the default one.
1578+ if (confirmation_type != "indicator-sound")
1579+ return;
1580+
1581+ props.sets (Canberra.PROP_EVENT_ID, "audio-volume-change");
1582+ play_sound = true;
1583+ }
1584+
1585+ if ((variant = hints.lookup ("sound-name")) != null) {
1586+ props.sets (Canberra.PROP_EVENT_ID, variant.get_string ());
1587+ play_sound = true;
1588+ }
1589+
1590+ if ((variant = hints.lookup ("sound-file")) != null) {
1591+ props.sets (Canberra.PROP_MEDIA_FILENAME, variant.get_string ());
1592+ play_sound = true;
1593+ }
1594+
1595+ // pick a sound according to the category
1596+ if (!play_sound) {
1597+ variant = hints.lookup ("category");
1598+ string? sound_name = null;
1599+
1600+ if (variant != null)
1601+ sound_name = category_to_sound (variant.get_string ());
1602+ else
1603+ sound_name = "dialog-information";
1604+
1605+ if (sound_name != null) {
1606+ props.sets (Canberra.PROP_EVENT_ID, sound_name);
1607+ play_sound = true;
1608+ }
1609+ }
1610+
1611+ if (play_sound)
1612+ ca_context.play_full (0, props);
1613+ }
1614+
1615+ static string? category_to_sound (string category)
1616+ {
1617+ string? sound = null;
1618+
1619+ switch (category) {
1620+ case "device.added":
1621+ sound = "device-added";
1622+ break;
1623+ case "device.removed":
1624+ sound = "device-removed";
1625+ break;
1626+ case "im":
1627+ sound = "message";
1628+ break;
1629+ case "im.received":
1630+ sound = "message-new-instant";
1631+ break;
1632+ case "network.connected":
1633+ sound = "network-connectivity-established";
1634+ break;
1635+ case "network.disconnected":
1636+ sound = "network-connectivity-lost";
1637+ break;
1638+ case "presence.online":
1639+ sound = "service-login";
1640+ break;
1641+ case "presence.offline":
1642+ sound = "service-logout";
1643+ break;
1644+ // no sound at all
1645+ case "x-gnome.music":
1646+ sound = null;
1647+ break;
1648+ // generic errors
1649+ case "device.error":
1650+ case "email.bounced":
1651+ case "im.error":
1652+ case "network.error":
1653+ case "transfer.error":
1654+ sound = "dialog-error";
1655+ break;
1656+ // use generic default
1657+ case "network":
1658+ case "email":
1659+ case "email.arrived":
1660+ case "presence":
1661+ case "transfer":
1662+ case "transfer.complete":
1663+ default:
1664+ sound = "dialog-information";
1665+ break;
1666+ }
1667+
1668+ return sound;
1669+ }
1670+
1671+ void notification_closed_callback (Notification notification, uint32 id, uint32 reason)
1672+ {
1673+ notification.closed.disconnect (notification_closed_callback);
1674+
1675+ notification_closed (id, reason);
1676+ }
1677+ }
1678+}
1679+
1680
1681=== added directory 'plugins/notify/data'
1682=== added file 'plugins/notify/data/image-mask.png'
1683Binary files plugins/notify/data/image-mask.png 1970-01-01 00:00:00 +0000 and plugins/notify/data/image-mask.png 2014-08-09 09:56:08 +0000 differ

Subscribers

People subscribed via source and target branches