1 /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 *
3 * Copyright (C) 2006-2007 Christian Hammond <chipx86@chipx86.com>
4 * Copyright (C) 2009 Red Hat, Inc.
5 * Copyright (C) 2011 Perberos <perberos@gmail.com>
6 * Copyright (C) 2012-2021 MATE Developers
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2, or (at your option)
11 * any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
21 * 02110-1301, USA.
22 */
23 #include "config.h"
24
25 #include <glib/gi18n.h>
26 #include <gtk/gtk.h>
27
28 #include <libxml/xpath.h>
29
30 typedef void (*ActionInvokedCb) (GtkWindow* nw, const char* key);
31 typedef void (*UrlClickedCb) (GtkWindow* nw, const char* url);
32
33 typedef struct {
34 GtkWidget* win;
35 GtkWidget* top_spacer;
36 GtkWidget* bottom_spacer;
37 GtkWidget* main_hbox;
38 GtkWidget* iconbox;
39 GtkWidget* icon;
40 GtkWidget* content_hbox;
41 GtkWidget* summary_label;
42 GtkWidget* close_button;
43 GtkWidget* body_label;
44 GtkWidget* actions_box;
45 GtkWidget* last_sep;
46 GtkWidget* stripe_spacer;
47 GtkWidget* pie_countdown;
48
49 gboolean has_arrow;
50 gboolean composited;
51 gboolean action_icons;
52
53 int point_x;
54 int point_y;
55
56 int drawn_arrow_begin_x;
57 int drawn_arrow_begin_y;
58 int drawn_arrow_middle_x;
59 int drawn_arrow_middle_y;
60 int drawn_arrow_end_x;
61 int drawn_arrow_end_y;
62
63 int width;
64 int height;
65
66 GdkPoint* border_points;
67 size_t num_border_points;
68
69 cairo_region_t *window_region;
70
71 guchar urgency;
72 glong timeout;
73 glong remaining;
74
75 UrlClickedCb url_clicked;
76
77 } WindowData;
78
79 enum {
80 URGENCY_LOW,
81 URGENCY_NORMAL,
82 URGENCY_CRITICAL
83 };
84
85 gboolean theme_check_init(unsigned int major_ver, unsigned int minor_ver,
86 unsigned int micro_ver);
87 void get_theme_info(char **theme_name, char **theme_ver, char **author,
88 char **homepage);
89 GtkWindow* create_notification(UrlClickedCb url_clicked);
90 void set_notification_text(GtkWindow *nw, const char *summary,
91 const char *body);
92 void set_notification_icon(GtkWindow *nw, GdkPixbuf *pixbuf);
93 void set_notification_arrow(GtkWidget *nw, gboolean visible, int x, int y);
94 void add_notification_action(GtkWindow *nw, const char *text, const char *key,
95 ActionInvokedCb cb);
96 void clear_notification_actions(GtkWindow *nw);
97 void move_notification(GtkWidget *nw, int x, int y);
98 void set_notification_timeout(GtkWindow *nw, glong timeout);
99 void set_notification_hints(GtkWindow *nw, GVariant *hints);
100 void notification_tick(GtkWindow *nw, glong remaining);
101
102 //#define ENABLE_GRADIENT_LOOK
103
104 #ifdef ENABLE_GRADIENT_LOOK
105 #define STRIPE_WIDTH 45
106 #else
107 #define STRIPE_WIDTH 30
108 #endif
109
110 #define WIDTH 400
111 #define IMAGE_SIZE 32
112 #define IMAGE_PADDING 10
113 #define SPACER_LEFT 30
114 #define PIE_RADIUS 12
115 #define PIE_WIDTH (2 * PIE_RADIUS)
116 #define PIE_HEIGHT (2 * PIE_RADIUS)
117 #define BODY_X_OFFSET (IMAGE_SIZE + 8)
118 #define DEFAULT_ARROW_OFFSET (SPACER_LEFT + 2)
119 #define DEFAULT_ARROW_HEIGHT 14
120 #define DEFAULT_ARROW_WIDTH 28
121 #define BACKGROUND_OPACITY 0.92
122 #define BOTTOM_GRADIENT_HEIGHT 30
123
124 static void
get_background_color(GtkStyleContext * context,GtkStateFlags state,GdkRGBA * color)125 get_background_color (GtkStyleContext *context,
126 GtkStateFlags state,
127 GdkRGBA *color)
128 {
129 GdkRGBA *c;
130
131 g_return_if_fail (color != NULL);
132 g_return_if_fail (GTK_IS_STYLE_CONTEXT (context));
133
134 gtk_style_context_get (context, state,
135 "background-color", &c,
136 NULL);
137
138 *color = *c;
139 gdk_rgba_free (c);
140 }
141
fill_background(GtkWidget * widget,WindowData * windata,cairo_t * cr)142 static void fill_background(GtkWidget* widget, WindowData* windata, cairo_t* cr)
143 {
144 GtkStyleContext *context;
145 GdkRGBA bg;
146
147 GtkAllocation allocation;
148
149 gtk_widget_get_allocation(widget, &allocation);
150
151 #ifdef ENABLE_GRADIENT_LOOK
152
153 cairo_pattern_t *gradient;
154 int gradient_y;
155
156 gradient_y = allocation.height - BOTTOM_GRADIENT_HEIGHT;
157
158 #endif
159
160 context = gtk_widget_get_style_context (windata->win);
161
162 gtk_style_context_save (context);
163 gtk_style_context_set_state (context, GTK_STATE_FLAG_NORMAL);
164
165 get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
166
167 gtk_style_context_restore (context);
168
169 if (windata->composited)
170 {
171 cairo_set_source_rgba(cr, bg.red, bg.green, bg.blue, BACKGROUND_OPACITY);
172 }
173 else
174 {
175 gdk_cairo_set_source_rgba (cr, &bg);
176 }
177
178 cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
179
180 cairo_fill(cr);
181
182 #ifdef ENABLE_GRADIENT_LOOK
183 /* Add a very subtle gradient to the bottom of the notification */
184 gradient = cairo_pattern_create_linear(0, gradient_y, 0, allocation.height);
185 cairo_pattern_add_color_stop_rgba(gradient, 0, 0, 0, 0, 0);
186 cairo_pattern_add_color_stop_rgba(gradient, 1, 0, 0, 0, 0.15);
187 cairo_rectangle(cr, 0, gradient_y, allocation.width, BOTTOM_GRADIENT_HEIGHT);
188
189 cairo_set_source(cr, gradient);
190 cairo_fill(cr);
191 cairo_pattern_destroy(gradient);
192 #endif
193 }
194
draw_stripe(GtkWidget * widget,WindowData * windata,cairo_t * cr)195 static void draw_stripe(GtkWidget* widget, WindowData* windata, cairo_t* cr)
196 {
197 GtkStyleContext* context;
198 GdkRGBA bg;
199 int stripe_x;
200 int stripe_y;
201 int stripe_height;
202 #ifdef ENABLE_GRADIENT_LOOK
203 cairo_pattern_t* gradient;
204 double r, g, b;
205 #endif
206
207 context = gtk_widget_get_style_context (widget);
208
209 gtk_style_context_save (context);
210
211 GtkAllocation alloc;
212 gtk_widget_get_allocation(windata->main_hbox, &alloc);
213
214 stripe_x = alloc.x + 1;
215
216 if (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL)
217 {
218 stripe_x = windata->width - STRIPE_WIDTH - stripe_x;
219 }
220
221 stripe_y = alloc.y + 1;
222 stripe_height = alloc.height - 2;
223
224 switch (windata->urgency)
225 {
226 case URGENCY_LOW: // LOW
227 gtk_style_context_set_state (context, GTK_STATE_FLAG_NORMAL);
228 gtk_style_context_add_class (context, GTK_STYLE_CLASS_VIEW);
229 get_background_color (context, GTK_STATE_FLAG_NORMAL, &bg);
230 gdk_cairo_set_source_rgba (cr, &bg);
231 break;
232
233 case URGENCY_CRITICAL: // CRITICAL
234 gdk_rgba_parse (&bg, "#CC0000");
235 break;
236
237 case URGENCY_NORMAL: // NORMAL
238 default:
239 gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
240 gtk_style_context_add_class (context, GTK_STYLE_CLASS_VIEW);
241 get_background_color (context, GTK_STATE_FLAG_SELECTED, &bg);
242 gdk_cairo_set_source_rgba (cr, &bg);
243 break;
244 }
245
246 gtk_style_context_restore (context);
247
248 cairo_rectangle(cr, stripe_x, stripe_y, STRIPE_WIDTH, stripe_height);
249
250 #ifdef ENABLE_GRADIENT_LOOK
251 r = color.red / 65535.0;
252 g = color.green / 65535.0;
253 b = color.blue / 65535.0;
254
255 gradient = cairo_pattern_create_linear(stripe_x, 0, STRIPE_WIDTH, 0);
256 cairo_pattern_add_color_stop_rgba(gradient, 0, r, g, b, 1);
257 cairo_pattern_add_color_stop_rgba(gradient, 1, r, g, b, 0);
258 cairo_set_source(cr, gradient);
259 cairo_fill(cr);
260 cairo_pattern_destroy(gradient);
261 #else
262 gdk_cairo_set_source_rgba (cr, &bg);
263 cairo_fill(cr);
264 #endif
265 }
266
get_notification_arrow_type(GtkWidget * nw)267 static GtkArrowType get_notification_arrow_type(GtkWidget* nw)
268 {
269 WindowData* windata;
270 GdkScreen* screen;
271 GdkRectangle monitor_geometry;
272 GdkDisplay* display;
273 GdkMonitor* monitor;
274
275 windata = g_object_get_data(G_OBJECT(nw), "windata");
276
277 screen = gdk_window_get_screen(GDK_WINDOW( gtk_widget_get_window(nw)));
278 display = gdk_screen_get_display (screen);
279 monitor = gdk_display_get_monitor_at_point (display, windata->point_x, windata->point_y);
280 gdk_monitor_get_geometry (monitor, &monitor_geometry);
281
282 if (windata->point_y - monitor_geometry.y + windata->height + DEFAULT_ARROW_HEIGHT > monitor_geometry.height)
283 {
284 return GTK_ARROW_DOWN;
285 }
286 else
287 {
288 return GTK_ARROW_UP;
289 }
290 }
291
292 #define ADD_POINT(_x, _y, shapeoffset_x, shapeoffset_y) \
293 G_STMT_START { \
294 windata->border_points[i].x = (_x); \
295 windata->border_points[i].y = (_y); \
296 shape_points[i].x = (_x) + (shapeoffset_x); \
297 shape_points[i].y = (_y) + (shapeoffset_y); \
298 i++;\
299 } G_STMT_END
300
create_border_with_arrow(GtkWidget * nw,WindowData * windata)301 static void create_border_with_arrow(GtkWidget* nw, WindowData* windata)
302 {
303 int width;
304 int height;
305 int y;
306 int norm_point_x;
307 int norm_point_y;
308 GtkArrowType arrow_type;
309 GdkScreen* screen;
310 int arrow_side1_width = DEFAULT_ARROW_WIDTH / 2;
311 int arrow_side2_width = DEFAULT_ARROW_WIDTH / 2;
312 int arrow_offset = DEFAULT_ARROW_OFFSET;
313 GdkPoint* shape_points = NULL;
314 int i = 0;
315 GdkMonitor* monitor;
316 GdkDisplay* display;
317 GdkRectangle monitor_geometry;
318
319 width = windata->width;
320 height = windata->height;
321
322 screen = gdk_window_get_screen(GDK_WINDOW(gtk_widget_get_window(nw)));
323 display = gdk_screen_get_display (screen);
324 monitor = gdk_display_get_monitor_at_point (display, windata->point_x, windata->point_y);
325 gdk_monitor_get_geometry (monitor, &monitor_geometry);
326
327 windata->num_border_points = 5;
328
329 arrow_type = get_notification_arrow_type(windata->win);
330
331 norm_point_x = windata->point_x - monitor_geometry.x;
332 norm_point_y = windata->point_y - monitor_geometry.y;
333
334 /* Handle the offset and such */
335 switch (arrow_type)
336 {
337 case GTK_ARROW_UP:
338 case GTK_ARROW_DOWN:
339
340 if (norm_point_x < arrow_side1_width)
341 {
342 arrow_side1_width = 0;
343 arrow_offset = 0;
344 }
345 else if (norm_point_x > monitor_geometry.width - arrow_side2_width)
346 {
347 arrow_side2_width = 0;
348 arrow_offset = width - arrow_side1_width;
349 }
350 else
351 {
352 if (norm_point_x - arrow_side2_width + width >= monitor_geometry.width)
353 {
354 arrow_offset = width - monitor_geometry.width + norm_point_x;
355 }
356 else
357 {
358 arrow_offset = MIN(norm_point_x - arrow_side1_width, DEFAULT_ARROW_OFFSET);
359 }
360
361 if (arrow_offset == 0 || arrow_offset == width - arrow_side1_width)
362 {
363 windata->num_border_points++;
364 }
365 else
366 {
367 windata->num_border_points += 2;
368 }
369 }
370
371 /*
372 * Why risk this for official builds? If it's somehow off the
373 * screen, it won't horribly impact the user. Definitely less
374 * than an assertion would...
375 */
376 #if 0
377 g_assert(arrow_offset + arrow_side1_width >= 0);
378 g_assert(arrow_offset + arrow_side1_width + arrow_side2_width <= width);
379 #endif
380
381 windata->border_points = g_new0(GdkPoint, windata->num_border_points);
382 shape_points = g_new0(GdkPoint, windata->num_border_points);
383
384 windata->drawn_arrow_begin_x = arrow_offset;
385 windata->drawn_arrow_middle_x = arrow_offset + arrow_side1_width;
386 windata->drawn_arrow_end_x = arrow_offset + arrow_side1_width + arrow_side2_width;
387
388 if (arrow_type == GTK_ARROW_UP)
389 {
390 windata->drawn_arrow_begin_y = DEFAULT_ARROW_HEIGHT;
391 windata->drawn_arrow_middle_y = 0;
392 windata->drawn_arrow_end_y = DEFAULT_ARROW_HEIGHT;
393
394 if (arrow_side1_width == 0)
395 {
396 ADD_POINT(0, 0, 0, 0);
397 }
398 else
399 {
400 ADD_POINT(0, DEFAULT_ARROW_HEIGHT, 0, 0);
401
402 if (arrow_offset > 0)
403 {
404 ADD_POINT(arrow_offset - (arrow_side2_width > 0 ? 0 : 1), DEFAULT_ARROW_HEIGHT, 0, 0);
405 }
406
407 ADD_POINT(arrow_offset + arrow_side1_width - (arrow_side2_width > 0 ? 0 : 1), 0, 0, 0);
408 }
409
410 if (arrow_side2_width > 0)
411 {
412 ADD_POINT(windata->drawn_arrow_end_x, windata->drawn_arrow_end_y, 1, 0);
413 ADD_POINT(width - 1, DEFAULT_ARROW_HEIGHT, 1, 0);
414 }
415
416 ADD_POINT(width - 1, height - 1, 1, 1);
417 ADD_POINT(0, height - 1, 0, 1);
418
419 y = windata->point_y;
420 }
421 else
422 {
423 windata->drawn_arrow_begin_y = height - DEFAULT_ARROW_HEIGHT;
424 windata->drawn_arrow_middle_y = height;
425 windata->drawn_arrow_end_y = height - DEFAULT_ARROW_HEIGHT;
426
427 ADD_POINT(0, 0, 0, 0);
428 ADD_POINT(width - 1, 0, 1, 0);
429
430 if (arrow_side2_width == 0)
431 {
432 ADD_POINT(width - 1, height, (arrow_side1_width > 0 ? 0 : 1), 0);
433 }
434 else
435 {
436 ADD_POINT(width - 1, height - DEFAULT_ARROW_HEIGHT, 1, 1);
437
438 if (arrow_offset < width - arrow_side1_width)
439 {
440 ADD_POINT(arrow_offset + arrow_side1_width + arrow_side2_width, height - DEFAULT_ARROW_HEIGHT, 0, 1);
441 }
442
443 ADD_POINT(arrow_offset + arrow_side1_width, height, 0, 1);
444 }
445
446 if (arrow_side1_width > 0)
447 {
448 ADD_POINT(windata->drawn_arrow_begin_x - (arrow_side2_width > 0 ? 0 : 1), windata->drawn_arrow_begin_y, 0, 0);
449 ADD_POINT(0, height - DEFAULT_ARROW_HEIGHT, 0, 1);
450 }
451
452 y = windata->point_y - height;
453 }
454
455 #if 0
456 g_assert(i == windata->num_border_points);
457 g_assert(windata->point_x - arrow_offset - arrow_side1_width >= 0);
458 #endif
459
460 gtk_window_move(GTK_WINDOW(windata->win), windata->point_x - arrow_offset - arrow_side1_width, y);
461
462 break;
463
464 case GTK_ARROW_LEFT:
465 case GTK_ARROW_RIGHT:
466
467 if (norm_point_y < arrow_side1_width)
468 {
469 arrow_side1_width = 0;
470 arrow_offset = norm_point_y;
471 }
472 else if (norm_point_y > monitor_geometry.height - arrow_side2_width)
473 {
474 arrow_side2_width = 0;
475 arrow_offset = norm_point_y - arrow_side1_width;
476 }
477 break;
478
479 default:
480 g_assert_not_reached();
481 }
482
483 g_assert(shape_points != NULL);
484
485 /* FIXME won't work with GTK+3, need a replacement */
486 /*windata->window_region = gdk_region_polygon(shape_points, windata->num_border_points, GDK_EVEN_ODD_RULE);*/
487 g_free(shape_points);
488 }
489
draw_border(GtkWidget * widget,WindowData * windata,cairo_t * cr)490 static void draw_border(GtkWidget* widget, WindowData *windata, cairo_t* cr)
491 {
492 cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 1.0);
493 cairo_set_line_width(cr, 1.0);
494
495 if (windata->has_arrow)
496 {
497 size_t i;
498
499 create_border_with_arrow(windata->win, windata);
500
501 cairo_move_to(cr, windata->border_points[0].x + 0.5, windata->border_points[0].y + 0.5);
502
503 for (i = 1; i < windata->num_border_points; i++)
504 {
505 cairo_line_to(cr, windata->border_points[i].x + 0.5, windata->border_points[i].y + 0.5);
506 }
507
508 cairo_close_path(cr);
509 /* FIXME window_region is not set up anyway, see previous fixme */
510 /*gdk_window_shape_combine_region (gtk_widget_get_window (windata->win), windata->window_region, 0, 0);*/
511 g_free(windata->border_points);
512 windata->border_points = NULL;
513 }
514 else
515 {
516 cairo_rectangle(cr, 0.5, 0.5, windata->width - 0.5, windata->height - 0.5);
517 }
518
519 cairo_stroke(cr);
520 }
521
522 static void
paint_window(GtkWidget * widget,cairo_t * cr,WindowData * windata)523 paint_window (GtkWidget *widget,
524 cairo_t *cr,
525 WindowData *windata)
526 {
527 cairo_t* cr2;
528 cairo_surface_t* surface;
529 GtkAllocation allocation;
530
531 gtk_widget_get_allocation(windata->win, &allocation);
532
533 if (windata->width == 0)
534 {
535 windata->width = allocation.width;
536 windata->height = allocation.height;
537 }
538
539 cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
540
541 gtk_widget_get_allocation(widget, &allocation);
542
543 surface = cairo_surface_create_similar (cairo_get_target (cr),
544 CAIRO_CONTENT_COLOR_ALPHA,
545 allocation.width,
546 allocation.height);
547
548 cr2 = cairo_create (surface);
549
550 fill_background(widget, windata, cr2);
551 draw_border(widget, windata, cr2);
552 draw_stripe(widget, windata, cr2);
553 cairo_fill (cr2);
554 cairo_destroy (cr2);
555
556 cairo_set_source_surface (cr, surface, 0, 0);
557 cairo_paint(cr);
558 cairo_surface_destroy(surface);
559 }
560
561 static gboolean
on_draw(GtkWidget * widget,cairo_t * cr,WindowData * windata)562 on_draw (GtkWidget *widget, cairo_t *cr, WindowData *windata)
563 {
564 paint_window (widget, cr, windata);
565
566 return FALSE;
567 }
568
destroy_windata(WindowData * windata)569 static void destroy_windata(WindowData* windata)
570 {
571 if (windata->window_region != NULL)
572 {
573 cairo_region_destroy(windata->window_region);
574 }
575
576 g_free(windata);
577 }
578
update_spacers(GtkWidget * nw)579 static void update_spacers(GtkWidget* nw)
580 {
581 WindowData* windata;
582
583 windata = g_object_get_data(G_OBJECT(nw), "windata");
584
585 if (windata->has_arrow)
586 {
587 switch (get_notification_arrow_type(GTK_WIDGET(nw)))
588 {
589 case GTK_ARROW_UP:
590 gtk_widget_show(windata->top_spacer);
591 gtk_widget_hide(windata->bottom_spacer);
592 break;
593
594 case GTK_ARROW_DOWN:
595 gtk_widget_hide(windata->top_spacer);
596 gtk_widget_show(windata->bottom_spacer);
597 break;
598
599 default:
600 g_assert_not_reached();
601 }
602 }
603 else
604 {
605 gtk_widget_hide(windata->top_spacer);
606 gtk_widget_hide(windata->bottom_spacer);
607 }
608 }
609
update_content_hbox_visibility(WindowData * windata)610 static void update_content_hbox_visibility(WindowData* windata)
611 {
612 /*
613 * This is all a hack, but until we have a libview-style ContentBox,
614 * it'll just have to do.
615 */
616 if (gtk_widget_get_visible(windata->icon) || gtk_widget_get_visible(windata->body_label) || gtk_widget_get_visible(windata->actions_box))
617 {
618 gtk_widget_show(windata->content_hbox);
619 }
620 else
621 {
622 gtk_widget_hide(windata->content_hbox);
623 }
624 }
625
configure_event_cb(GtkWidget * nw,GdkEventConfigure * event,WindowData * windata)626 static gboolean configure_event_cb(GtkWidget* nw, GdkEventConfigure* event, WindowData* windata)
627 {
628 windata->width = event->width;
629 windata->height = event->height;
630
631 update_spacers(nw);
632 gtk_widget_queue_draw(nw);
633
634 return FALSE;
635 }
636
activate_link(GtkLabel * label,const char * url,WindowData * windata)637 static gboolean activate_link(GtkLabel* label, const char* url, WindowData* windata)
638 {
639 windata->url_clicked(GTK_WINDOW(windata->win), url);
640
641 return TRUE;
642 }
643
create_notification(UrlClickedCb url_clicked)644 GtkWindow* create_notification(UrlClickedCb url_clicked)
645 {
646 GtkWidget* spacer;
647 GtkWidget* win;
648 GtkWidget* main_vbox;
649 GtkWidget* hbox;
650 GtkWidget* vbox;
651 GtkWidget* close_button;
652 GtkWidget* image;
653 AtkObject* atkobj;
654 WindowData* windata;
655
656 GdkVisual *visual;
657 GdkScreen* screen;
658
659 windata = g_new0(WindowData, 1);
660 windata->urgency = URGENCY_NORMAL;
661 windata->url_clicked = url_clicked;
662
663 win = gtk_window_new(GTK_WINDOW_POPUP);
664 gtk_window_set_resizable(GTK_WINDOW(win), FALSE);
665 windata->win = win;
666
667 windata->composited = FALSE;
668
669
670 screen = gtk_window_get_screen(GTK_WINDOW(win));
671
672 visual = gdk_screen_get_rgba_visual(screen);
673
674 if (visual != NULL)
675 {
676 gtk_widget_set_visual(win, visual);
677
678 if (gdk_screen_is_composited(screen))
679 {
680 windata->composited = TRUE;
681 }
682 }
683
684 gtk_window_set_title(GTK_WINDOW(win), "Notification");
685 gtk_window_set_type_hint(GTK_WINDOW(win), GDK_WINDOW_TYPE_HINT_NOTIFICATION);
686 gtk_widget_add_events(win, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
687 gtk_widget_realize(win);
688 gtk_widget_set_size_request(win, WIDTH, -1);
689
690 g_object_set_data_full(G_OBJECT(win), "windata", windata, (GDestroyNotify) destroy_windata);
691 atk_object_set_role(gtk_widget_get_accessible(win), ATK_ROLE_ALERT);
692
693 g_signal_connect(G_OBJECT(win), "configure_event", G_CALLBACK(configure_event_cb), windata);
694
695 main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
696 gtk_widget_show(main_vbox);
697 gtk_container_add (GTK_CONTAINER (win), main_vbox);
698 gtk_container_set_border_width(GTK_CONTAINER(main_vbox), 1);
699
700 g_signal_connect (G_OBJECT (main_vbox), "draw", G_CALLBACK (on_draw), windata);
701
702 windata->top_spacer = gtk_image_new();
703 gtk_box_pack_start(GTK_BOX(main_vbox), windata->top_spacer, FALSE, FALSE, 0);
704 gtk_widget_set_size_request(windata->top_spacer, -1, DEFAULT_ARROW_HEIGHT);
705
706 windata->main_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
707 gtk_widget_show(windata->main_hbox);
708 gtk_box_pack_start(GTK_BOX(main_vbox), windata->main_hbox, FALSE, FALSE, 0);
709
710 windata->bottom_spacer = gtk_image_new();
711 gtk_box_pack_start(GTK_BOX(main_vbox), windata->bottom_spacer, FALSE, FALSE, 0);
712 gtk_widget_set_size_request(windata->bottom_spacer, -1, DEFAULT_ARROW_HEIGHT);
713
714 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
715 gtk_widget_show(vbox);
716 gtk_box_pack_start(GTK_BOX(windata->main_hbox), vbox, TRUE, TRUE, 0);
717 gtk_container_set_border_width(GTK_CONTAINER(vbox), 10);
718
719 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
720 gtk_widget_show(hbox);
721 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
722
723 spacer = gtk_image_new();
724 gtk_widget_show(spacer);
725 gtk_box_pack_start(GTK_BOX(hbox), spacer, FALSE, FALSE, 0);
726 gtk_widget_set_size_request(spacer, SPACER_LEFT, -1);
727
728 windata->summary_label = gtk_label_new(NULL);
729 gtk_widget_show(windata->summary_label);
730 gtk_box_pack_start(GTK_BOX(hbox), windata->summary_label, TRUE, TRUE, 0);
731 gtk_label_set_xalign (GTK_LABEL (windata->summary_label), 0.0);
732 gtk_label_set_yalign (GTK_LABEL (windata->summary_label), 0.0);
733 gtk_label_set_line_wrap(GTK_LABEL(windata->summary_label), TRUE);
734 gtk_label_set_line_wrap_mode (GTK_LABEL (windata->summary_label), PANGO_WRAP_WORD_CHAR);
735
736 atkobj = gtk_widget_get_accessible(windata->summary_label);
737 atk_object_set_description (atkobj, _("Notification summary text."));
738
739 /* Add the close button */
740 close_button = gtk_button_new();
741 windata->close_button = close_button;
742 gtk_widget_set_halign (close_button, GTK_ALIGN_END);
743 gtk_widget_set_valign (close_button, GTK_ALIGN_START);
744 gtk_widget_show(close_button);
745 gtk_box_pack_start(GTK_BOX(hbox), close_button, FALSE, FALSE, 0);
746 gtk_button_set_relief(GTK_BUTTON(close_button), GTK_RELIEF_NONE);
747 gtk_container_set_border_width(GTK_CONTAINER(close_button), 0);
748 //gtk_widget_set_size_request(close_button, 20, 20);
749 g_signal_connect_swapped(G_OBJECT(close_button), "clicked", G_CALLBACK(gtk_widget_destroy), win);
750
751 atkobj = gtk_widget_get_accessible(close_button);
752 atk_action_set_description(ATK_ACTION(atkobj), 0,
753 _("Closes the notification."));
754 atk_object_set_name(atkobj, "");
755 atk_object_set_description (atkobj, _("Closes the notification."));
756
757 image = gtk_image_new_from_icon_name ("window-close", GTK_ICON_SIZE_MENU);
758 gtk_widget_show(image);
759 gtk_container_add(GTK_CONTAINER(close_button), image);
760
761 windata->content_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
762 gtk_box_pack_start(GTK_BOX(vbox), windata->content_hbox, FALSE, FALSE, 0);
763
764 windata->iconbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
765 gtk_widget_show(windata->iconbox);
766 gtk_box_pack_start(GTK_BOX(windata->content_hbox), windata->iconbox, FALSE, FALSE, 0);
767 gtk_widget_set_size_request(windata->iconbox, BODY_X_OFFSET, -1);
768
769 windata->icon = gtk_image_new();
770 gtk_box_pack_start(GTK_BOX(windata->iconbox), windata->icon, TRUE, TRUE, 0);
771 gtk_widget_set_halign (windata->icon, GTK_ALIGN_CENTER);
772 gtk_widget_set_valign (windata->icon, GTK_ALIGN_START);
773
774 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
775 gtk_widget_show(vbox);
776 gtk_box_pack_start(GTK_BOX(windata->content_hbox), vbox, TRUE, TRUE, 0);
777
778 windata->body_label = gtk_label_new(NULL);
779 gtk_widget_show(windata->body_label);
780 gtk_box_pack_start(GTK_BOX(vbox), windata->body_label, TRUE, TRUE, 0);
781 gtk_label_set_xalign (GTK_LABEL (windata->body_label), 0.0);
782 gtk_label_set_yalign (GTK_LABEL (windata->body_label), 0.0);
783 gtk_label_set_line_wrap(GTK_LABEL(windata->body_label), TRUE);
784 gtk_label_set_line_wrap_mode (GTK_LABEL (windata->body_label), PANGO_WRAP_WORD_CHAR);
785 gtk_label_set_max_width_chars (GTK_LABEL (windata->body_label), 50);
786 g_signal_connect(G_OBJECT(windata->body_label), "activate-link", G_CALLBACK(activate_link), windata);
787
788 atkobj = gtk_widget_get_accessible(windata->body_label);
789 atk_object_set_description (atkobj, _("Notification body text."));
790
791 windata->actions_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
792 gtk_widget_set_halign (windata->actions_box, GTK_ALIGN_END);
793 gtk_widget_show(windata->actions_box);
794 gtk_box_pack_start(GTK_BOX(vbox), windata->actions_box, FALSE, TRUE, 0);
795
796 return GTK_WINDOW(win);
797 }
798
set_notification_hints(GtkWindow * nw,GVariant * hints)799 void set_notification_hints(GtkWindow *nw, GVariant *hints)
800 {
801 WindowData *windata = g_object_get_data(G_OBJECT(nw), "windata");
802 guint8 urgency;
803 gboolean action_icons;
804
805 g_assert(windata != NULL);
806
807 if (g_variant_lookup(hints, "urgency", "y", &urgency))
808 {
809 windata->urgency = urgency;
810
811 if (windata->urgency == URGENCY_CRITICAL) {
812 gtk_window_set_title(GTK_WINDOW(nw), "Critical Notification");
813 } else {
814 gtk_window_set_title(GTK_WINDOW(nw), "Notification");
815 }
816 }
817
818 /* Determine if action-icons have been requested */
819 if (g_variant_lookup(hints, "action-icons", "b", &action_icons))
820 {
821 windata->action_icons = action_icons;
822 }
823 }
824
set_notification_timeout(GtkWindow * nw,glong timeout)825 void set_notification_timeout(GtkWindow* nw, glong timeout)
826 {
827 WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
828
829 g_assert(windata != NULL);
830
831 windata->timeout = timeout;
832 }
833
notification_tick(GtkWindow * nw,glong remaining)834 void notification_tick(GtkWindow* nw, glong remaining)
835 {
836 WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
837
838 windata->remaining = remaining;
839
840 if (windata->pie_countdown != NULL)
841 {
842 gtk_widget_queue_draw_area(windata->pie_countdown, 0, 0, PIE_WIDTH, PIE_HEIGHT);
843 }
844 }
845
set_notification_text(GtkWindow * nw,const char * summary,const char * body)846 void set_notification_text(GtkWindow* nw, const char* summary, const char* body)
847 {
848 char* str;
849 size_t str_len;
850 char* quoted;
851 GtkRequisition req;
852 WindowData* windata;
853
854 windata = g_object_get_data(G_OBJECT(nw), "windata");
855 g_assert(windata != NULL);
856
857 quoted = g_markup_escape_text(summary, -1);
858 str = g_strdup_printf("<b><big>%s</big></b>", quoted);
859 g_free(quoted);
860
861 gtk_label_set_markup(GTK_LABEL(windata->summary_label), str);
862 g_free(str);
863
864 /* body */
865 xmlDocPtr doc;
866 xmlInitParser();
867 str = g_strconcat ("<markup>", body, "</markup>", NULL);
868 /* parse notification body */
869 str_len = strlen (str);
870 doc = xmlReadMemory(str, (int) str_len, "noname.xml", NULL, 0);
871 g_free (str);
872 if (doc != NULL) {
873 xmlXPathContextPtr xpathCtx;
874 xmlXPathObjectPtr xpathObj;
875 xmlNodeSetPtr nodes;
876 const char *body_label_text;
877 int i, size;
878
879 /* filterout img nodes */
880 xpathCtx = xmlXPathNewContext(doc);
881 xpathObj = xmlXPathEvalExpression((unsigned char *)"//img", xpathCtx);
882 nodes = xpathObj->nodesetval;
883 size = (nodes) ? nodes->nodeNr : 0;
884 for(i = size - 1; i >= 0; i--) {
885 xmlUnlinkNode (nodes->nodeTab[i]);
886 xmlFreeNode (nodes->nodeTab[i]);
887 }
888
889 /* write doc to string */
890 xmlBufferPtr buf = xmlBufferCreate();
891 (void) xmlNodeDump(buf, doc, xmlDocGetRootElement (doc), 0, 0);
892 str = (char *)buf->content;
893 gtk_label_set_markup (GTK_LABEL (windata->body_label), str);
894
895 /* cleanup */
896 xmlBufferFree (buf);
897 xmlXPathFreeObject (xpathObj);
898 xmlXPathFreeContext (xpathCtx);
899 xmlFreeDoc (doc);
900
901 /* Does it render properly? */
902 body_label_text = gtk_label_get_text (GTK_LABEL (windata->body_label));
903 if ((body_label_text == NULL) || (strlen (body_label_text) == 0)) {
904 goto render_fail;
905 }
906 goto renrer_ok;
907 }
908
909 render_fail:
910 /* could not parse notification body */
911 quoted = g_markup_escape_text(body, -1);
912 gtk_label_set_markup (GTK_LABEL (windata->body_label), quoted);
913 g_free (quoted);
914
915 renrer_ok:
916 xmlCleanupParser ();
917
918 if (body == NULL || *body == '\0')
919 gtk_widget_hide(windata->body_label);
920 else
921 gtk_widget_show(windata->body_label);
922
923 update_content_hbox_visibility(windata);
924
925 if (body != NULL && *body != '\0')
926 {
927 gtk_widget_get_preferred_size (windata->iconbox, NULL, &req);
928 /* -1: border width for
929 * -6: spacing for hbox */
930 gtk_widget_set_size_request(windata->body_label, WIDTH - (1 * 2) - (10 * 2) - req.width - 6, -1);
931 }
932
933 gtk_widget_get_preferred_size (windata->close_button, NULL, &req);
934 /* -1: main_vbox border width
935 * -10: vbox border width
936 * -6: spacing for hbox */
937 gtk_widget_set_size_request(windata->summary_label, WIDTH - (1 * 2) - (10 * 2) - SPACER_LEFT - req.width - (6 * 2), -1);
938 }
939
set_notification_icon(GtkWindow * nw,GdkPixbuf * pixbuf)940 void set_notification_icon(GtkWindow* nw, GdkPixbuf* pixbuf)
941 {
942 WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
943
944 g_assert(windata != NULL);
945
946 gtk_image_set_from_pixbuf(GTK_IMAGE(windata->icon), pixbuf);
947
948 if (pixbuf != NULL)
949 {
950 int pixbuf_width = gdk_pixbuf_get_width(pixbuf);
951
952 gtk_widget_show(windata->icon);
953 gtk_widget_set_size_request(windata->iconbox, MAX(BODY_X_OFFSET, pixbuf_width), -1);
954 }
955 else
956 {
957 gtk_widget_hide(windata->icon);
958 gtk_widget_set_size_request(windata->iconbox, BODY_X_OFFSET, -1);
959 }
960
961 update_content_hbox_visibility(windata);
962 }
963
set_notification_arrow(GtkWidget * nw,gboolean visible,int x,int y)964 void set_notification_arrow(GtkWidget* nw, gboolean visible, int x, int y)
965 {
966 WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
967
968 g_assert(windata != NULL);
969
970 windata->has_arrow = visible;
971 windata->point_x = x;
972 windata->point_y = y;
973
974 update_spacers(nw);
975 }
976
977 static void
paint_countdown(GtkWidget * pie,cairo_t * cr,WindowData * windata)978 paint_countdown (GtkWidget *pie,
979 cairo_t *cr,
980 WindowData *windata)
981 {
982 GtkStyleContext *context;
983 GdkRGBA bg;
984 GtkAllocation alloc;
985 cairo_t* cr2;
986 cairo_surface_t* surface;
987
988 context = gtk_widget_get_style_context (windata->win);
989
990 gtk_style_context_save (context);
991 gtk_style_context_set_state (context, GTK_STATE_FLAG_SELECTED);
992
993 get_background_color (context, GTK_STATE_FLAG_SELECTED, &bg);
994
995 gtk_style_context_restore (context);
996
997 gtk_widget_get_allocation(pie, &alloc);
998 cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
999 surface = cairo_surface_create_similar (cairo_get_target(cr),
1000 CAIRO_CONTENT_COLOR_ALPHA,
1001 alloc.width,
1002 alloc.height);
1003
1004 cr2 = cairo_create (surface);
1005
1006 fill_background (pie, windata, cr2);
1007
1008 if (windata->timeout > 0)
1009 {
1010 gdouble pct = (gdouble) windata->remaining / (gdouble) windata->timeout;
1011
1012 gdk_cairo_set_source_rgba (cr2, &bg);
1013
1014 cairo_move_to (cr2, PIE_RADIUS, PIE_RADIUS);
1015 cairo_arc_negative (cr2, PIE_RADIUS, PIE_RADIUS, PIE_RADIUS, -G_PI_2, -(pct * G_PI * 2) - G_PI_2);
1016 cairo_line_to (cr2, PIE_RADIUS, PIE_RADIUS);
1017 cairo_fill (cr2);
1018 }
1019
1020 cairo_destroy(cr2);
1021
1022 cairo_save (cr);
1023 cairo_set_source_surface (cr, surface, 0, 0);
1024 cairo_paint (cr);
1025 cairo_restore (cr);
1026
1027 cairo_surface_destroy(surface);
1028 }
1029
1030 static gboolean
on_countdown_draw(GtkWidget * widget,cairo_t * cr,WindowData * windata)1031 on_countdown_draw (GtkWidget *widget, cairo_t *cr, WindowData *windata)
1032 {
1033 paint_countdown (widget, cr, windata);
1034
1035 return FALSE;
1036 }
1037
action_clicked_cb(GtkWidget * w,GdkEventButton * event,ActionInvokedCb action_cb)1038 static void action_clicked_cb(GtkWidget* w, GdkEventButton* event, ActionInvokedCb action_cb)
1039 {
1040 GtkWindow* nw;
1041 const char* key;
1042 nw = g_object_get_data(G_OBJECT(w), "_nw");
1043 key = g_object_get_data(G_OBJECT(w), "_action_key");
1044 action_cb(nw, key);
1045 }
1046
add_notification_action(GtkWindow * nw,const char * text,const char * key,ActionInvokedCb cb)1047 void add_notification_action(GtkWindow* nw, const char* text, const char* key, ActionInvokedCb cb)
1048 {
1049 WindowData* windata;
1050 GtkWidget* label;
1051 GtkWidget* button;
1052 GtkWidget* hbox;
1053 GdkPixbuf* pixbuf;
1054 char* buf;
1055
1056 windata = g_object_get_data(G_OBJECT(nw), "windata");
1057
1058 g_assert(windata != NULL);
1059
1060 if (gtk_widget_get_visible(windata->actions_box))
1061 {
1062 gtk_widget_show(windata->actions_box);
1063 update_content_hbox_visibility(windata);
1064
1065 /* Don't try to re-add a pie_countdown */
1066 if (!windata->pie_countdown) {
1067 windata->pie_countdown = gtk_drawing_area_new();
1068 gtk_widget_set_halign (windata->pie_countdown, GTK_ALIGN_END);
1069 gtk_widget_show(windata->pie_countdown);
1070
1071 gtk_box_pack_end (GTK_BOX (windata->actions_box), windata->pie_countdown, FALSE, TRUE, 0);
1072 gtk_widget_set_size_request(windata->pie_countdown,
1073 PIE_WIDTH, PIE_HEIGHT);
1074 g_signal_connect(G_OBJECT(windata->pie_countdown), "draw",
1075 G_CALLBACK(on_countdown_draw), windata);
1076 }
1077 }
1078
1079 if (windata->action_icons) {
1080 button = gtk_button_new_from_icon_name(key, GTK_ICON_SIZE_BUTTON);
1081 goto add_button;
1082 }
1083
1084 button = gtk_button_new();
1085 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
1086 gtk_widget_show(hbox);
1087 gtk_container_add(GTK_CONTAINER(button), hbox);
1088
1089 /* Try to be smart and find a suitable icon. */
1090 buf = g_strdup_printf("stock_%s", key);
1091 pixbuf = gtk_icon_theme_load_icon(gtk_icon_theme_get_for_screen(gdk_window_get_screen(gtk_widget_get_window(GTK_WIDGET(nw)))),
1092 buf, 16, GTK_ICON_LOOKUP_USE_BUILTIN, NULL);
1093 g_free(buf);
1094
1095 if (pixbuf != NULL)
1096 {
1097 GtkWidget* image = gtk_image_new_from_pixbuf(pixbuf);
1098 gtk_widget_show(image);
1099 gtk_box_pack_start(GTK_BOX(hbox), image, FALSE, FALSE, 0);
1100 gtk_widget_set_halign (image, GTK_ALIGN_CENTER);
1101 gtk_widget_set_valign (image, GTK_ALIGN_CENTER);
1102 }
1103
1104 label = gtk_label_new(NULL);
1105 gtk_widget_show(label);
1106 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
1107 gtk_label_set_xalign (GTK_LABEL (label), 0.0);
1108 gtk_label_set_yalign (GTK_LABEL (label), 0.5);
1109 buf = g_strdup_printf("<small>%s</small>", text);
1110 gtk_label_set_markup(GTK_LABEL(label), buf);
1111 g_free(buf);
1112
1113 add_button:
1114 gtk_widget_show(button);
1115 gtk_box_pack_start(GTK_BOX(windata->actions_box), button, FALSE, FALSE, 0);
1116
1117 g_object_set_data(G_OBJECT(button), "_nw", nw);
1118 g_object_set_data_full(G_OBJECT(button), "_action_key", g_strdup(key), g_free);
1119 g_signal_connect(G_OBJECT(button), "button-release-event", G_CALLBACK(action_clicked_cb), cb);
1120
1121 gtk_widget_show_all(windata->actions_box);
1122 }
1123
clear_notification_actions(GtkWindow * nw)1124 void clear_notification_actions(GtkWindow* nw)
1125 {
1126 WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
1127
1128 windata->pie_countdown = NULL;
1129
1130 gtk_widget_hide(windata->actions_box);
1131 gtk_container_foreach(GTK_CONTAINER(windata->actions_box), (GtkCallback) gtk_widget_destroy, NULL);
1132 }
1133
move_notification(GtkWidget * nw,int x,int y)1134 void move_notification(GtkWidget* nw, int x, int y)
1135 {
1136 WindowData* windata = g_object_get_data(G_OBJECT(nw), "windata");
1137
1138 g_assert(windata != NULL);
1139
1140 if (windata->has_arrow)
1141 {
1142 gtk_widget_queue_resize(nw);
1143 }
1144 else
1145 {
1146 gtk_window_move(GTK_WINDOW(nw), x, y);
1147 }
1148 }
1149
get_theme_info(char ** theme_name,char ** theme_ver,char ** author,char ** homepage)1150 void get_theme_info(char** theme_name, char** theme_ver, char** author, char** homepage)
1151 {
1152 *theme_name = g_strdup("Standard");
1153
1154 /* If they are constants, maybe we can remove printf and use G_STRINGIFY() */
1155 *theme_ver = g_strdup_printf("%d.%d.%d", NOTIFICATION_DAEMON_MAJOR_VERSION, NOTIFICATION_DAEMON_MINOR_VERSION, NOTIFICATION_DAEMON_MICRO_VERSION);
1156 *author = g_strdup("Christian Hammond");
1157 *homepage = g_strdup("http://www.galago-project.org/");
1158 }
1159
theme_check_init(unsigned int major_ver,unsigned int minor_ver,unsigned int micro_ver)1160 gboolean theme_check_init(unsigned int major_ver, unsigned int minor_ver, unsigned int micro_ver)
1161 {
1162 return major_ver == NOTIFICATION_DAEMON_MAJOR_VERSION && minor_ver == NOTIFICATION_DAEMON_MINOR_VERSION && micro_ver == NOTIFICATION_DAEMON_MICRO_VERSION;
1163 }
1164