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