1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2010 Red Hat, Inc.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18 *
19 */
20
21 #include "config.h"
22
23 #include <string.h>
24 #include <strings.h>
25 #include <glib.h>
26 #include <glib/gi18n.h>
27
28 #include "nd-notification.h"
29 #include "nd-bubble.h"
30
31 #define ND_BUBBLE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), ND_TYPE_BUBBLE, NdBubblePrivate))
32
33 #define EXPIRATION_TIME_DEFAULT -1
34 #define EXPIRATION_TIME_NEVER_EXPIRES 0
35 #define TIMEOUT_SEC 5
36
37 #define WIDTH 400
38 #define DEFAULT_X0 0
39 #define DEFAULT_Y0 0
40 #define DEFAULT_RADIUS 16
41 #define IMAGE_SIZE 48
42 #define BODY_X_OFFSET (IMAGE_SIZE + 8)
43 #define BACKGROUND_ALPHA 0.90
44
45 #define MAX_ICON_SIZE IMAGE_SIZE
46
47 struct NdBubblePrivate
48 {
49 NdNotification *notification;
50
51 GtkWidget *main_hbox;
52 GtkWidget *icon;
53 GtkWidget *content_hbox;
54 GtkWidget *summary_label;
55 GtkWidget *close_button;
56 GtkWidget *body_label;
57 GtkWidget *actions_box;
58 GtkWidget *last_sep;
59
60 int width;
61 int height;
62 int last_width;
63 int last_height;
64
65 gboolean have_icon;
66 gboolean have_body;
67 gboolean have_actions;
68
69 gboolean url_clicked_lock;
70
71 gboolean composited;
72 glong remaining;
73 guint timeout_id;
74 };
75
76 static void nd_bubble_finalize (GObject *object);
77 static void on_notification_changed (NdNotification *notification,
78 NdBubble *bubble);
79
G_DEFINE_TYPE(NdBubble,nd_bubble,GTK_TYPE_WINDOW)80 G_DEFINE_TYPE (NdBubble, nd_bubble, GTK_TYPE_WINDOW)
81
82 NdNotification *
83 nd_bubble_get_notification (NdBubble *bubble)
84 {
85 g_return_val_if_fail (ND_IS_BUBBLE (bubble), NULL);
86
87 return bubble->priv->notification;
88 }
89
90 static gboolean
nd_bubble_configure_event(GtkWidget * widget,GdkEventConfigure * event)91 nd_bubble_configure_event (GtkWidget *widget,
92 GdkEventConfigure *event)
93 {
94 NdBubble *bubble = ND_BUBBLE (widget);
95
96 bubble->priv->width = event->width;
97 bubble->priv->height = event->height;
98
99 gtk_widget_queue_draw (widget);
100
101 return FALSE;
102 }
103
104 static void
nd_bubble_composited_changed(GtkWidget * widget)105 nd_bubble_composited_changed (GtkWidget *widget)
106 {
107 NdBubble *bubble = ND_BUBBLE (widget);
108 GdkScreen *screen;
109 GdkVisual *visual;
110
111 bubble->priv->composited = gdk_screen_is_composited (gtk_widget_get_screen (widget));
112
113 screen = gtk_window_get_screen (GTK_WINDOW (bubble));
114 visual = gdk_screen_get_rgba_visual (screen);
115 if (visual == NULL) {
116 visual = gdk_screen_get_system_visual (screen);
117 }
118
119 gtk_widget_set_visual (GTK_WIDGET (bubble), visual);
120
121 gtk_widget_queue_draw (widget);
122 }
123
124 static void
draw_round_rect(cairo_t * cr,gdouble aspect,gdouble x,gdouble y,gdouble corner_radius,gdouble width,gdouble height)125 draw_round_rect (cairo_t *cr,
126 gdouble aspect,
127 gdouble x,
128 gdouble y,
129 gdouble corner_radius,
130 gdouble width,
131 gdouble height)
132 {
133 gdouble radius = corner_radius / aspect;
134
135 cairo_move_to (cr, x + radius, y);
136
137 // top-right, left of the corner
138 cairo_line_to (cr,
139 x + width - radius,
140 y);
141
142 // top-right, below the corner
143 cairo_arc (cr,
144 x + width - radius,
145 y + radius,
146 radius,
147 -90.0f * G_PI / 180.0f,
148 0.0f * G_PI / 180.0f);
149
150 // bottom-right, above the corner
151 cairo_line_to (cr,
152 x + width,
153 y + height - radius);
154
155 // bottom-right, left of the corner
156 cairo_arc (cr,
157 x + width - radius,
158 y + height - radius,
159 radius,
160 0.0f * G_PI / 180.0f,
161 90.0f * G_PI / 180.0f);
162
163 // bottom-left, right of the corner
164 cairo_line_to (cr,
165 x + radius,
166 y + height);
167
168 // bottom-left, above the corner
169 cairo_arc (cr,
170 x + radius,
171 y + height - radius,
172 radius,
173 90.0f * G_PI / 180.0f,
174 180.0f * G_PI / 180.0f);
175
176 // top-left, below the corner
177 cairo_line_to (cr,
178 x,
179 y + radius);
180
181 // top-left, right of the corner
182 cairo_arc (cr,
183 x + radius,
184 y + radius,
185 radius,
186 180.0f * G_PI / 180.0f,
187 270.0f * G_PI / 180.0f);
188 }
189
190 static void
get_background_color(GtkStyleContext * context,GtkStateFlags state,GdkRGBA * color)191 get_background_color (GtkStyleContext *context,
192 GtkStateFlags state,
193 GdkRGBA *color)
194 {
195 GdkRGBA *c;
196
197 g_return_if_fail (color != NULL);
198 g_return_if_fail (GTK_IS_STYLE_CONTEXT (context));
199
200 gtk_style_context_get (context, state,
201 "background-color", &c,
202 NULL);
203
204 *color = *c;
205 gdk_rgba_free (c);
206 }
207
208 static void
paint_bubble(NdBubble * bubble,cairo_t * cr)209 paint_bubble (NdBubble *bubble,
210 cairo_t *cr)
211 {
212 GtkStyleContext *context;
213 GdkRGBA bg;
214 GdkRGBA fg;
215 cairo_t *cr2;
216 cairo_surface_t *surface;
217 cairo_region_t *region;
218 GtkAllocation allocation;
219
220 gtk_widget_get_allocation (GTK_WIDGET (bubble), &allocation);
221 if (bubble->priv->width == 0 || bubble->priv->height == 0) {
222 bubble->priv->width = MAX (allocation.width, 1);
223 bubble->priv->height = MAX (allocation.height, 1);
224 }
225
226 surface = cairo_surface_create_similar (cairo_get_target (cr),
227 CAIRO_CONTENT_COLOR_ALPHA,
228 bubble->priv->width,
229 bubble->priv->height);
230 cr2 = cairo_create (surface);
231
232 /* transparent background */
233 cairo_rectangle (cr2, 0, 0, bubble->priv->width, bubble->priv->height);
234 cairo_set_source_rgba (cr2, 0.0, 0.0, 0.0, 0.0);
235 cairo_fill (cr2);
236
237 draw_round_rect (cr2,
238 1.0f,
239 DEFAULT_X0 + 1,
240 DEFAULT_Y0 + 1,
241 DEFAULT_RADIUS,
242 allocation.width - 2,
243 allocation.height - 2);
244
245 context = gtk_widget_get_style_context (GTK_WIDGET (bubble));
246
247 gtk_style_context_save (context);
248 gtk_style_context_set_state (context, GTK_STATE_FLAG_NORMAL);
249
250 get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
251 gtk_style_context_get_color (context, GTK_STATE_FLAG_NORMAL, &fg);
252
253 gtk_style_context_restore (context);
254
255 cairo_set_source_rgba (cr2, bg.red, bg.green, bg.blue,
256 BACKGROUND_ALPHA);
257 cairo_fill_preserve (cr2);
258
259 cairo_set_source_rgba (cr2, fg.red, fg.green, fg.blue,
260 BACKGROUND_ALPHA / 2);
261 cairo_set_line_width (cr2, 2);
262 cairo_stroke (cr2);
263
264 cairo_destroy (cr2);
265
266 cairo_save (cr);
267 cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
268 cairo_set_source_surface (cr, surface, 0, 0);
269 cairo_paint (cr);
270 cairo_restore (cr);
271
272 if (bubble->priv->width == bubble->priv->last_width
273 && bubble->priv->height == bubble->priv->last_height) {
274 goto done;
275 }
276
277 /* Don't shape when composited */
278 if (bubble->priv->composited) {
279 gtk_widget_shape_combine_region (GTK_WIDGET (bubble), NULL);
280 goto done;
281 }
282
283 bubble->priv->last_width = bubble->priv->width;
284 bubble->priv->last_height = bubble->priv->height;
285
286 region = gdk_cairo_region_create_from_surface (surface);
287 gtk_widget_shape_combine_region (GTK_WIDGET (bubble), region);
288 cairo_region_destroy (region);
289
290 done:
291 cairo_surface_destroy (surface);
292
293 }
294
295 static gboolean
nd_bubble_draw(GtkWidget * widget,cairo_t * cr)296 nd_bubble_draw (GtkWidget *widget,
297 cairo_t *cr)
298 {
299 NdBubble *bubble = ND_BUBBLE (widget);
300
301 paint_bubble (bubble, cr);
302
303 GTK_WIDGET_CLASS (nd_bubble_parent_class)->draw (widget, cr);
304
305 return FALSE;
306 }
307
308 static gboolean
nd_bubble_button_release_event(GtkWidget * widget,GdkEventButton * event)309 nd_bubble_button_release_event (GtkWidget *widget,
310 GdkEventButton *event)
311 {
312 NdBubble *bubble = ND_BUBBLE (widget);
313
314 if (bubble->priv->url_clicked_lock) {
315 bubble->priv->url_clicked_lock = FALSE;
316 return FALSE;
317 }
318
319 nd_notification_action_invoked (bubble->priv->notification, "default");
320 gtk_widget_destroy (GTK_WIDGET (bubble));
321
322 return FALSE;
323 }
324
325 static gboolean
timeout_bubble(NdBubble * bubble)326 timeout_bubble (NdBubble *bubble)
327 {
328 bubble->priv->timeout_id = 0;
329
330 /* FIXME: if transient also close it */
331
332 gtk_widget_destroy (GTK_WIDGET (bubble));
333
334 return FALSE;
335 }
336
337 static void
add_timeout(NdBubble * bubble)338 add_timeout (NdBubble *bubble)
339 {
340 int timeout = nd_notification_get_timeout(bubble->priv->notification);
341
342 if (bubble->priv->timeout_id != 0) {
343 g_source_remove (bubble->priv->timeout_id);
344 bubble->priv->timeout_id = 0;
345 }
346
347 if (timeout == EXPIRATION_TIME_NEVER_EXPIRES)
348 return;
349
350 if (timeout == EXPIRATION_TIME_DEFAULT)
351 timeout = TIMEOUT_SEC * 1000;
352
353 bubble->priv->timeout_id = g_timeout_add (timeout,
354 (GSourceFunc) timeout_bubble,
355 bubble);
356 }
357
358 static void
nd_bubble_realize(GtkWidget * widget)359 nd_bubble_realize (GtkWidget *widget)
360 {
361 NdBubble *bubble = ND_BUBBLE (widget);
362
363 add_timeout (bubble);
364
365 GTK_WIDGET_CLASS (nd_bubble_parent_class)->realize (widget);
366 }
367
368 static void
nd_bubble_get_preferred_width(GtkWidget * widget,gint * min_width,gint * nat_width)369 nd_bubble_get_preferred_width (GtkWidget *widget,
370 gint *min_width,
371 gint *nat_width)
372 {
373 if (nat_width != NULL) {
374 *nat_width = WIDTH;
375 }
376 }
377
378 static gboolean
nd_bubble_motion_notify_event(GtkWidget * widget,GdkEventMotion * event)379 nd_bubble_motion_notify_event (GtkWidget *widget,
380 GdkEventMotion *event)
381 {
382 NdBubble *bubble = ND_BUBBLE (widget);
383
384 add_timeout (bubble);
385
386 return FALSE;
387 }
388
389 static void
nd_bubble_class_init(NdBubbleClass * klass)390 nd_bubble_class_init (NdBubbleClass *klass)
391 {
392 GObjectClass *object_class = G_OBJECT_CLASS (klass);
393 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
394
395 object_class->finalize = nd_bubble_finalize;
396
397 widget_class->draw = nd_bubble_draw;
398 widget_class->configure_event = nd_bubble_configure_event;
399 widget_class->composited_changed = nd_bubble_composited_changed;
400 widget_class->button_release_event = nd_bubble_button_release_event;
401 widget_class->motion_notify_event = nd_bubble_motion_notify_event;
402 widget_class->realize = nd_bubble_realize;
403 widget_class->get_preferred_width = nd_bubble_get_preferred_width;
404
405 g_type_class_add_private (klass, sizeof (NdBubblePrivate));
406 }
407
408 static gboolean
on_activate_link(GtkLabel * label,char * uri,NdBubble * bubble)409 on_activate_link (GtkLabel *label,
410 char *uri,
411 NdBubble *bubble)
412 {
413 char *escaped_uri;
414 char *cmd = NULL;
415 char *found = NULL;
416
417 /* Somewhat of a hack.. */
418 bubble->priv->url_clicked_lock = TRUE;
419
420 escaped_uri = g_shell_quote (uri);
421
422 if ((found = g_find_program_in_path ("gvfs-open")) != NULL) {
423 cmd = g_strdup_printf ("gvfs-open %s", escaped_uri);
424 } else if ((found = g_find_program_in_path ("xdg-open")) != NULL) {
425 cmd = g_strdup_printf ("xdg-open %s", escaped_uri);
426 } else if ((found = g_find_program_in_path ("firefox")) != NULL) {
427 cmd = g_strdup_printf ("firefox %s", escaped_uri);
428 } else {
429 g_warning ("Unable to find a browser.");
430 }
431
432 g_free (escaped_uri);
433 g_free (found);
434
435 if (cmd != NULL) {
436 g_spawn_command_line_async (cmd, NULL);
437 g_free (cmd);
438 }
439
440 return TRUE;
441 }
442
443 static void
on_close_button_clicked(GtkButton * button,NdBubble * bubble)444 on_close_button_clicked (GtkButton *button,
445 NdBubble *bubble)
446 {
447 nd_notification_close (bubble->priv->notification, ND_NOTIFICATION_CLOSED_USER);
448 gtk_widget_destroy (GTK_WIDGET (bubble));
449 }
450
451 static void
nd_bubble_init(NdBubble * bubble)452 nd_bubble_init (NdBubble *bubble)
453 {
454 GtkWidget *main_vbox;
455 GtkWidget *vbox;
456 GtkWidget *close_button;
457 GtkWidget *image;
458 AtkObject *atkobj;
459 GdkScreen *screen;
460 GdkVisual *visual;
461
462 bubble->priv = ND_BUBBLE_GET_PRIVATE (bubble);
463
464 gtk_widget_add_events (GTK_WIDGET (bubble), GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
465 atk_object_set_role (gtk_widget_get_accessible (GTK_WIDGET (bubble)), ATK_ROLE_ALERT);
466
467 screen = gtk_window_get_screen (GTK_WINDOW (bubble));
468 visual = gdk_screen_get_rgba_visual (screen);
469 if (visual == NULL) {
470 visual = gdk_screen_get_system_visual (screen);
471 }
472
473 gtk_widget_set_visual (GTK_WIDGET (bubble), visual);
474
475 if (gdk_screen_is_composited (screen)) {
476 bubble->priv->composited = TRUE;
477 }
478
479 main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
480 gtk_widget_show (main_vbox);
481 gtk_container_add (GTK_CONTAINER (bubble), main_vbox);
482 gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12);
483
484 bubble->priv->main_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
485 gtk_widget_show (bubble->priv->main_hbox);
486 gtk_box_pack_start (GTK_BOX (main_vbox),
487 bubble->priv->main_hbox,
488 FALSE, FALSE, 0);
489
490 /* Add icon */
491
492 bubble->priv->icon = gtk_image_new ();
493 gtk_widget_set_valign (bubble->priv->icon, GTK_ALIGN_START);
494 gtk_widget_set_margin_top (bubble->priv->icon, 5);
495 gtk_widget_set_size_request (bubble->priv->icon, BODY_X_OFFSET, -1);
496 gtk_widget_show (bubble->priv->icon);
497
498 gtk_box_pack_start (GTK_BOX (bubble->priv->main_hbox),
499 bubble->priv->icon,
500 FALSE, FALSE, 0);
501
502 /* Add vbox */
503
504 vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
505 gtk_widget_show (vbox);
506 gtk_box_pack_start (GTK_BOX (bubble->priv->main_hbox), vbox, TRUE, TRUE, 0);
507 gtk_container_set_border_width (GTK_CONTAINER (vbox), 10);
508
509 /* Add the close button */
510
511 close_button = gtk_button_new ();
512 gtk_widget_set_valign (close_button, GTK_ALIGN_START);
513 gtk_widget_show (close_button);
514
515 bubble->priv->close_button = close_button;
516 gtk_box_pack_start (GTK_BOX (bubble->priv->main_hbox),
517 bubble->priv->close_button,
518 FALSE, FALSE, 0);
519
520 gtk_button_set_relief (GTK_BUTTON (close_button), GTK_RELIEF_NONE);
521 gtk_container_set_border_width (GTK_CONTAINER (close_button), 0);
522 g_signal_connect (G_OBJECT (close_button),
523 "clicked",
524 G_CALLBACK (on_close_button_clicked),
525 bubble);
526
527 atkobj = gtk_widget_get_accessible (close_button);
528 atk_action_set_description (ATK_ACTION (atkobj), 0,
529 _("Closes the notification."));
530 atk_object_set_name (atkobj, "");
531 atk_object_set_description (atkobj, _("Closes the notification."));
532
533 image = gtk_image_new_from_icon_name ("window-close", GTK_ICON_SIZE_MENU);
534 gtk_widget_show (image);
535 gtk_container_add (GTK_CONTAINER (close_button), image);
536
537 /* center vbox */
538 bubble->priv->summary_label = gtk_label_new (NULL);
539 gtk_widget_show (bubble->priv->summary_label);
540 gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->summary_label, TRUE, TRUE, 0);
541 gtk_label_set_xalign (GTK_LABEL (bubble->priv->summary_label), 0.0);
542 gtk_label_set_yalign (GTK_LABEL (bubble->priv->summary_label), 0.0);
543 gtk_label_set_line_wrap (GTK_LABEL (bubble->priv->summary_label), TRUE);
544 gtk_label_set_line_wrap_mode (GTK_LABEL (bubble->priv->summary_label), PANGO_WRAP_WORD_CHAR);
545
546 atkobj = gtk_widget_get_accessible (bubble->priv->summary_label);
547 atk_object_set_description (atkobj, _("Notification summary text."));
548
549 bubble->priv->content_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
550 gtk_widget_show (bubble->priv->content_hbox);
551 gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->content_hbox, FALSE, FALSE, 0);
552
553
554 vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
555 gtk_widget_show (vbox);
556 gtk_box_pack_start (GTK_BOX (bubble->priv->content_hbox), vbox, TRUE, TRUE, 0);
557
558 bubble->priv->body_label = gtk_label_new (NULL);
559 gtk_widget_show (bubble->priv->body_label);
560 gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->body_label, TRUE, TRUE, 0);
561 gtk_label_set_xalign (GTK_LABEL (bubble->priv->body_label), 0.0);
562 gtk_label_set_yalign (GTK_LABEL (bubble->priv->body_label), 0.0);
563 gtk_label_set_line_wrap (GTK_LABEL (bubble->priv->body_label), TRUE);
564 gtk_label_set_line_wrap_mode (GTK_LABEL (bubble->priv->body_label), PANGO_WRAP_WORD_CHAR);
565 g_signal_connect (bubble->priv->body_label,
566 "activate-link",
567 G_CALLBACK (on_activate_link),
568 bubble);
569
570 atkobj = gtk_widget_get_accessible (bubble->priv->body_label);
571 atk_object_set_description (atkobj, _("Notification summary text."));
572
573 bubble->priv->actions_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
574 gtk_widget_set_halign (bubble->priv->actions_box, GTK_ALIGN_END);
575 gtk_widget_show (bubble->priv->actions_box);
576
577 gtk_box_pack_start (GTK_BOX (vbox), bubble->priv->actions_box, FALSE, TRUE, 0);
578 }
579
580 static void
nd_bubble_finalize(GObject * object)581 nd_bubble_finalize (GObject *object)
582 {
583 NdBubble *bubble;
584
585 g_return_if_fail (object != NULL);
586 g_return_if_fail (ND_IS_BUBBLE (object));
587
588 bubble = ND_BUBBLE (object);
589
590 g_return_if_fail (bubble->priv != NULL);
591
592 if (bubble->priv->timeout_id != 0) {
593 g_source_remove (bubble->priv->timeout_id);
594 }
595
596 g_signal_handlers_disconnect_by_func (bubble->priv->notification, G_CALLBACK (on_notification_changed), bubble);
597
598 g_object_unref (bubble->priv->notification);
599
600 G_OBJECT_CLASS (nd_bubble_parent_class)->finalize (object);
601 }
602
603 static void
update_content_hbox_visibility(NdBubble * bubble)604 update_content_hbox_visibility (NdBubble *bubble)
605 {
606 if (bubble->priv->have_icon
607 || bubble->priv->have_body
608 || bubble->priv->have_actions) {
609 gtk_widget_show (bubble->priv->content_hbox);
610 } else {
611 gtk_widget_hide (bubble->priv->content_hbox);
612 }
613 }
614
615 static void
set_notification_text(NdBubble * bubble,const char * summary,const char * body)616 set_notification_text (NdBubble *bubble,
617 const char *summary,
618 const char *body)
619 {
620 char *str;
621 char *quoted;
622 GtkRequisition req;
623 int summary_width;
624
625 quoted = g_markup_escape_text (summary, -1);
626 str = g_strdup_printf ("<b><big>%s</big></b>", quoted);
627 g_free (quoted);
628
629 gtk_label_set_markup (GTK_LABEL (bubble->priv->summary_label), str);
630
631 g_free (str);
632 gtk_widget_show_all (GTK_WIDGET (bubble));
633
634 if (pango_parse_markup (body, -1, 0, NULL, NULL, NULL, NULL))
635 gtk_label_set_markup (GTK_LABEL (bubble->priv->body_label), body);
636 else {
637 gchar *tmp;
638
639 tmp = g_markup_escape_text (body, -1);
640 gtk_label_set_text (GTK_LABEL (bubble->priv->body_label), body);
641 g_free (tmp);
642 }
643
644 if (body == NULL || *body == '\0') {
645 bubble->priv->have_body = FALSE;
646 gtk_widget_hide (bubble->priv->body_label);
647 } else {
648 bubble->priv->have_body = TRUE;
649 gtk_widget_show (bubble->priv->body_label);
650 }
651 update_content_hbox_visibility (bubble);
652
653 gtk_widget_get_preferred_size (bubble->priv->close_button, NULL, &req);
654 /* -1: main_vbox border width
655 -10: vbox border width
656 -6: spacing for hbox */
657 summary_width = WIDTH - (1*2) - (10*2) - BODY_X_OFFSET - req.width - (6*2);
658
659 if (body != NULL && *body != '\0') {
660 gtk_widget_set_size_request (bubble->priv->body_label,
661 summary_width,
662 -1);
663 }
664
665 gtk_widget_set_size_request (bubble->priv->summary_label,
666 summary_width,
667 -1);
668 }
669
670 static GdkPixbuf *
scale_pixbuf(GdkPixbuf * pixbuf,int max_width,int max_height,gboolean no_stretch_hint)671 scale_pixbuf (GdkPixbuf *pixbuf,
672 int max_width,
673 int max_height,
674 gboolean no_stretch_hint)
675 {
676 int pw;
677 int ph;
678 float scale_factor_x = 1.0;
679 float scale_factor_y = 1.0;
680 float scale_factor = 1.0;
681
682 pw = gdk_pixbuf_get_width (pixbuf);
683 ph = gdk_pixbuf_get_height (pixbuf);
684
685 /* Determine which dimension requires the smallest scale. */
686 scale_factor_x = (float) max_width / (float) pw;
687 scale_factor_y = (float) max_height / (float) ph;
688
689 if (scale_factor_x > scale_factor_y) {
690 scale_factor = scale_factor_y;
691 } else {
692 scale_factor = scale_factor_x;
693 }
694
695 /* always scale down, allow to disable scaling up */
696 if (scale_factor < 1.0 || !no_stretch_hint) {
697 int scale_x;
698 int scale_y;
699
700 scale_x = (int) (pw * scale_factor);
701 scale_y = (int) (ph * scale_factor);
702 return gdk_pixbuf_scale_simple (pixbuf,
703 scale_x,
704 scale_y,
705 GDK_INTERP_BILINEAR);
706 } else {
707 return g_object_ref (pixbuf);
708 }
709 }
710
711 static void
set_notification_icon(NdBubble * bubble,GdkPixbuf * pixbuf)712 set_notification_icon (NdBubble *bubble,
713 GdkPixbuf *pixbuf)
714 {
715 GdkPixbuf *scaled;
716
717 scaled = NULL;
718 if (pixbuf != NULL) {
719 scaled = scale_pixbuf (pixbuf,
720 MAX_ICON_SIZE,
721 MAX_ICON_SIZE,
722 TRUE);
723 }
724
725 gtk_image_set_from_pixbuf (GTK_IMAGE (bubble->priv->icon), scaled);
726
727 if (scaled != NULL) {
728 int pixbuf_width = gdk_pixbuf_get_width (scaled);
729
730 gtk_widget_show (bubble->priv->icon);
731 gtk_widget_set_size_request (bubble->priv->icon,
732 MAX (BODY_X_OFFSET, pixbuf_width), -1);
733 g_object_unref (scaled);
734 bubble->priv->have_icon = TRUE;
735 } else {
736 gtk_widget_hide (bubble->priv->icon);
737 gtk_widget_set_size_request (bubble->priv->icon,
738 BODY_X_OFFSET,
739 -1);
740 bubble->priv->have_icon = FALSE;
741 }
742
743 update_content_hbox_visibility (bubble);
744 }
745
746 static void
on_action_clicked(GtkButton * button,GdkEventButton * event,NdBubble * bubble)747 on_action_clicked (GtkButton *button,
748 GdkEventButton *event,
749 NdBubble *bubble)
750 {
751 const char *key = g_object_get_data (G_OBJECT (button), "_action_key");
752 gboolean resident = nd_notification_get_is_resident (bubble->priv->notification);
753 gboolean transient = nd_notification_get_is_transient (bubble->priv->notification);
754
755 nd_notification_action_invoked (bubble->priv->notification,
756 key);
757
758 if (transient || !resident)
759 gtk_widget_destroy (GTK_WIDGET (bubble));
760 }
761
762 static void
add_notification_action(NdBubble * bubble,const char * text,const char * key)763 add_notification_action (NdBubble *bubble,
764 const char *text,
765 const char *key)
766 {
767 GtkWidget *button;
768 GtkWidget *hbox;
769 GdkPixbuf *pixbuf;
770 char *buf;
771
772 if (!gtk_widget_get_visible (bubble->priv->actions_box)) {
773 gtk_widget_show (bubble->priv->actions_box);
774 update_content_hbox_visibility (bubble);
775 }
776
777 button = gtk_button_new ();
778 gtk_widget_show (button);
779 gtk_box_pack_start (GTK_BOX (bubble->priv->actions_box), button, FALSE, FALSE, 0);
780 gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
781 gtk_container_set_border_width (GTK_CONTAINER (button), 0);
782
783 hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
784 gtk_widget_show (hbox);
785 gtk_container_add (GTK_CONTAINER (button), hbox);
786
787 pixbuf = NULL;
788 /* try to load an icon if requested */
789 if (nd_notification_get_action_icons (bubble->priv->notification)) {
790 pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_for_screen (gtk_widget_get_screen (GTK_WIDGET (bubble))),
791 key,
792 20,
793 GTK_ICON_LOOKUP_USE_BUILTIN,
794 NULL);
795 }
796
797 if (pixbuf != NULL) {
798 GtkWidget *image;
799
800 image = gtk_image_new_from_pixbuf (pixbuf);
801 g_object_unref (pixbuf);
802 atk_object_set_name (gtk_widget_get_accessible (GTK_WIDGET (button)),
803 text);
804 gtk_widget_show (image);
805 gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
806 gtk_widget_set_halign (image, GTK_ALIGN_CENTER);
807 gtk_widget_set_valign (image, GTK_ALIGN_CENTER);
808 } else {
809 GtkWidget *label;
810
811 label = gtk_label_new (NULL);
812 gtk_widget_show (label);
813 gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
814 gtk_label_set_xalign (GTK_LABEL (label), 0.0);
815 buf = g_strdup_printf ("<small>%s</small>", text);
816 gtk_label_set_markup (GTK_LABEL (label), buf);
817 g_free (buf);
818 }
819
820 g_object_set_data_full (G_OBJECT (button),
821 "_action_key", g_strdup (key), g_free);
822 g_signal_connect (G_OBJECT (button),
823 "button-release-event",
824 G_CALLBACK (on_action_clicked),
825 bubble);
826 }
827
828 static void
clear_actions(NdBubble * bubble)829 clear_actions (NdBubble *bubble)
830 {
831 gtk_widget_hide (bubble->priv->actions_box);
832 gtk_container_foreach (GTK_CONTAINER (bubble->priv->actions_box),
833 (GtkCallback)gtk_widget_destroy,
834 NULL);
835 bubble->priv->have_actions = FALSE;
836 }
837
838 static void
add_actions(NdBubble * bubble)839 add_actions (NdBubble *bubble)
840 {
841 char **actions;
842 int i;
843
844 actions = nd_notification_get_actions (bubble->priv->notification);
845
846 for (i = 0; actions[i] != NULL; i += 2) {
847 char *l = actions[i + 1];
848
849 if (l == NULL) {
850 g_warning ("Label not found for action %s. "
851 "The protocol specifies that a label must "
852 "follow an action in the actions array",
853 actions[i]);
854
855 break;
856 }
857
858 if (strcasecmp (actions[i], "default") != 0) {
859 add_notification_action (bubble,
860 l,
861 actions[i]);
862 bubble->priv->have_actions = TRUE;
863 }
864 }
865 }
866
867 static void
update_image(NdBubble * bubble)868 update_image (NdBubble *bubble)
869 {
870 GdkPixbuf *pixbuf;
871
872 pixbuf = nd_notification_load_image (bubble->priv->notification, IMAGE_SIZE);
873 if (pixbuf != NULL) {
874 set_notification_icon (bubble, pixbuf);
875 g_object_unref (G_OBJECT (pixbuf));
876 }
877 }
878
879 static void
update_bubble(NdBubble * bubble)880 update_bubble (NdBubble *bubble)
881 {
882 set_notification_text (bubble,
883 nd_notification_get_summary (bubble->priv->notification),
884 nd_notification_get_body (bubble->priv->notification));
885 clear_actions (bubble);
886 add_actions (bubble);
887 update_image (bubble);
888 update_content_hbox_visibility (bubble);
889
890 add_timeout (bubble);
891 }
892
893 static void
on_notification_changed(NdNotification * notification,NdBubble * bubble)894 on_notification_changed (NdNotification *notification,
895 NdBubble *bubble)
896 {
897 update_bubble (bubble);
898 }
899
900 NdBubble *
nd_bubble_new_for_notification(NdNotification * notification)901 nd_bubble_new_for_notification (NdNotification *notification)
902 {
903 NdBubble *bubble;
904
905 bubble = g_object_new (ND_TYPE_BUBBLE,
906 "app-paintable", TRUE,
907 "type", GTK_WINDOW_POPUP,
908 "title", "Notification",
909 "resizable", FALSE,
910 "type-hint", GDK_WINDOW_TYPE_HINT_NOTIFICATION,
911 NULL);
912
913 bubble->priv->notification = g_object_ref (notification);
914 g_signal_connect (notification, "changed", G_CALLBACK (on_notification_changed), bubble);
915 update_bubble (bubble);
916
917 return bubble;
918 }
919