Merge lp:~gala-dev/gala/notification-plugin into lp:gala
- notification-plugin
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Danielle Foré | Approve | ||
Review via email: mp+227416@code.launchpad.net |
Commit message
Implement notifications plugin
Description of the change
Danielle Foré (danrabbit) wrote : | # |
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
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.
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)
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
Preview Diff
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, "&"); |
571 | + text = tag_regex.replace (text, text.length, 0, "<"); |
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' |
1683 | Binary 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 |
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.