1 /*-*- Mode: C; c-basic-offset: 8 -*-*/
2 
3 /***
4   This file is part of libcanberra.
5 
6   Copyright 2008 Lennart Poettering
7 
8   libcanberra is free software; you can redistribute it and/or modify
9   it under the terms of the GNU Lesser General Public License as
10   published by the Free Software Foundation, either version 2.1 of the
11   License, or (at your option) any later version.
12 
13   libcanberra is distributed in the hope that it will be useful, but
14   WITHOUT ANY WARRANTY; without even the implied warranty of
15   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16   Lesser General Public License for more details.
17 
18   You should have received a copy of the GNU Lesser General Public
19   License along with libcanberra. If not, see
20   <http://www.gnu.org/licenses/>.
21 ***/
22 
23 #ifdef HAVE_CONFIG_H
24 #include <config.h>
25 #endif
26 
27 #include <gtk/gtk.h>
28 #include <gdk/gdkx.h>
29 #include <X11/Xatom.h>
30 
31 #include "canberra-gtk.h"
32 
33 typedef struct {
34         guint signal_id;
35         gboolean arg1_is_set;
36         GObject *object;
37         GValue arg1;
38         GdkEvent *event;
39 } SoundEventData;
40 
41 /*
42    We generate these sounds:
43 
44    dialog-error
45    dialog-warning
46    dialog-information
47    dialog-question
48    window-new
49    window-close
50    window-minimized
51    window-unminimized
52    window-maximized
53    window-unmaximized
54    notebook-tab-changed
55    dialog-ok
56    dialog-cancel
57    item-selected
58    link-pressed
59    link-released
60    button-pressed
61    button-released
62    menu-click
63    button-toggle-on
64    button-toggle-off
65    menu-popup
66    menu-popdown
67    menu-replace
68    tooltip-popup
69    tooltip-popdown
70    drag-start
71    drag-accept
72    drag-fail
73    expander-toggle-on
74    expander-toggle-off
75 
76    TODO:
77    scroll-xxx
78    window-switch
79    window-resize-xxx
80    window-move-xxx
81 
82 */
83 
84 static gboolean disabled = FALSE;
85 
86 static GQueue sound_event_queue = G_QUEUE_INIT;
87 
88 static guint idle_id = 0;
89 
90 static guint
91         signal_id_dialog_response,
92         signal_id_widget_show,
93         signal_id_widget_hide,
94         signal_id_check_menu_item_toggled,
95         signal_id_menu_item_activate,
96         signal_id_toggle_button_toggled,
97         signal_id_button_pressed,
98         signal_id_button_released,
99         signal_id_widget_window_state_event,
100         signal_id_notebook_switch_page,
101         signal_id_tree_view_cursor_changed,
102         signal_id_icon_view_selection_changed,
103         signal_id_widget_drag_begin,
104         signal_id_widget_drag_failed,
105         signal_id_widget_drag_drop,
106         signal_id_expander_activate;
107 
108 static GQuark
109         disable_sound_quark,
110         was_iconized_quark,
111         is_xembed_quark;
112 
113 /* Make sure GCC doesn't warn us about a missing prototype for this
114  * exported function */
115 void gtk_module_init(gint *argc, gchar ***argv[]);
116 
translate_message_tye(GtkMessageType mt)117 static const char *translate_message_tye(GtkMessageType mt) {
118         static const char *const message_type_table[] = {
119                 [GTK_MESSAGE_INFO] = "dialog-information",
120                 [GTK_MESSAGE_WARNING] = "dialog-warning",
121                 [GTK_MESSAGE_QUESTION] = "dialog-question",
122                 [GTK_MESSAGE_ERROR] = "dialog-error",
123                 [GTK_MESSAGE_OTHER] = NULL
124         };
125 
126         if (mt >= G_N_ELEMENTS(message_type_table))
127                 return NULL;
128 
129         return message_type_table[mt];
130 }
131 
translate_response(int response)132 static const char *translate_response(int response) {
133         static const char *const response_table[] = {
134                 [-GTK_RESPONSE_NONE] = NULL,
135                 [-GTK_RESPONSE_REJECT] = "dialog-cancel",
136                 [-GTK_RESPONSE_DELETE_EVENT] = "dialog-cancel",
137                 [-GTK_RESPONSE_ACCEPT] = "dialog-ok",
138                 [-GTK_RESPONSE_OK] = "dialog-ok",
139                 [-GTK_RESPONSE_CANCEL] = "dialog-cancel",
140                 [-GTK_RESPONSE_CLOSE] = "dialog-ok",
141                 [-GTK_RESPONSE_YES] = "dialog-ok",
142                 [-GTK_RESPONSE_NO] = "dialog-cancel",
143                 [-GTK_RESPONSE_APPLY] = "dialog-ok",
144                 [-GTK_RESPONSE_HELP] = NULL,
145         };
146 
147         if (response >= 0)
148                 return NULL;
149 
150         if ((unsigned) -response >= G_N_ELEMENTS(response_table))
151                 return NULL;
152 
153         return response_table[-response];
154 }
155 
is_child_of_combo_box(GtkWidget * w)156 static gboolean is_child_of_combo_box(GtkWidget *w) {
157 
158         while (w) {
159 
160                 if (GTK_IS_COMBO_BOX(w))
161                         return TRUE;
162 
163                 w = gtk_widget_get_parent(w);
164         }
165 
166         return FALSE;
167 }
168 
find_parent_dialog(GtkWidget * w)169 static GtkDialog* find_parent_dialog(GtkWidget *w) {
170 
171         while (w) {
172 
173                 if (GTK_IS_DIALOG(w))
174                         return GTK_DIALOG(w);
175 
176                 w = gtk_widget_get_parent(w);
177         }
178 
179         return NULL;
180 }
181 
free_sound_event(SoundEventData * d)182 static void free_sound_event(SoundEventData *d) {
183 
184         g_object_unref(d->object);
185 
186         if (d->arg1_is_set)
187                 g_value_unset(&d->arg1);
188 
189         if (d->event)
190                 gdk_event_free(d->event);
191 
192         g_slice_free(SoundEventData, d);
193 }
194 
is_menu_hint(GdkWindowTypeHint hint)195 static gboolean is_menu_hint(GdkWindowTypeHint hint) {
196         return
197                 hint == GDK_WINDOW_TYPE_HINT_POPUP_MENU ||
198                 hint == GDK_WINDOW_TYPE_HINT_DROPDOWN_MENU ||
199                 hint == GDK_WINDOW_TYPE_HINT_MENU;
200 }
201 
filter_sound_event(SoundEventData * d)202 static SoundEventData* filter_sound_event(SoundEventData *d) {
203         GList *i, *n;
204 
205         do {
206 
207                 for (i = sound_event_queue.head; i; i = n) {
208                         SoundEventData *j;
209 
210                         j = i->data;
211                         n = i->next;
212 
213                         if (d->object == j->object) {
214 
215                                 /* Let's drop a show event immediately followed by a
216                                  * hide event */
217 
218                                 if (d->signal_id == signal_id_widget_show &&
219                                     j->signal_id == signal_id_widget_hide) {
220 
221                                         free_sound_event(d);
222                                         free_sound_event(j);
223                                         g_queue_delete_link(&sound_event_queue, i);
224 
225                                         return NULL;
226                                 }
227 
228                                 /* Let's drop widget hide events in favour of dialog
229                                  * response.
230                                  *
231                                  * Let's drop widget window state events in favour of
232                                  * widget hide/show.
233                                  *
234                                  * Let's drop double events */
235 
236                                 if ((d->signal_id == signal_id_widget_hide &&
237                                      j->signal_id == signal_id_dialog_response) ||
238 
239                                     (d->signal_id == signal_id_widget_window_state_event &&
240                                      j->signal_id == signal_id_widget_hide) ||
241 
242                                     (d->signal_id == signal_id_widget_window_state_event &&
243                                      j->signal_id == signal_id_widget_show)) {
244 
245                                         free_sound_event(d);
246                                         d = j;
247                                         g_queue_delete_link(&sound_event_queue, i);
248                                         break;
249                                 }
250 
251                                 if ((d->signal_id == signal_id_dialog_response &&
252                                      j->signal_id == signal_id_widget_hide) ||
253 
254                                     (d->signal_id == signal_id_widget_show &&
255                                      j->signal_id == signal_id_widget_window_state_event) ||
256 
257                                     (d->signal_id == signal_id_widget_hide &&
258                                      j->signal_id == signal_id_widget_window_state_event) ||
259 
260                                     (d->signal_id == j->signal_id)) {
261 
262                                         free_sound_event(j);
263                                         g_queue_delete_link(&sound_event_queue, i);
264                                 }
265 
266                         } else if (GTK_IS_WINDOW(d->object) && GTK_IS_WINDOW(j->object)) {
267 
268                                 GdkWindowTypeHint dhint, jhint;
269 
270                                 dhint = gtk_window_get_type_hint(GTK_WINDOW(d->object));
271                                 jhint = gtk_window_get_type_hint(GTK_WINDOW(j->object));
272 
273                                 if (is_menu_hint(dhint) && is_menu_hint(jhint)) {
274 
275                                         if (d->signal_id == signal_id_widget_hide &&
276                                             j->signal_id == signal_id_widget_show) {
277                                                 free_sound_event(d);
278                                                 d = j;
279                                                 g_queue_delete_link(&sound_event_queue, i);
280                                                 break;
281                                         }
282 
283                                         if (d->signal_id == signal_id_widget_show &&
284                                             j->signal_id == signal_id_widget_hide) {
285 
286                                                 free_sound_event(j);
287                                                 g_queue_delete_link(&sound_event_queue, i);
288                                         }
289                                 }
290                         }
291                 }
292 
293                 /* If we exited the iteration early, let's retry. */
294 
295         } while (i);
296 
297         /* FIXME: Filter menu hide on menu show */
298 
299         return d;
300 }
301 
window_get_desktop(GdkDisplay * d,GdkWindow * w)302 static gint window_get_desktop(GdkDisplay *d, GdkWindow *w) {
303         Atom type_return;
304         gint format_return;
305         gulong nitems_return;
306         gulong bytes_after_return;
307         guchar *data = NULL;
308         gint ret = -1;
309 
310 #ifdef GDK_IS_X11_DISPLAY
311         if (!GDK_IS_X11_DISPLAY(d))
312                 return 0;
313 #endif
314 
315         if (XGetWindowProperty(GDK_DISPLAY_XDISPLAY(d), GDK_WINDOW_XID(w),
316                                gdk_x11_get_xatom_by_name_for_display(d, "_NET_WM_DESKTOP"),
317                                0, G_MAXLONG, False, XA_CARDINAL, &type_return,
318                                &format_return, &nitems_return, &bytes_after_return,
319                                &data) != Success)
320                 return -1;
321 
322         if (type_return == XA_CARDINAL && format_return == 32 && data) {
323                 guint32 desktop = *(guint32*) data;
324 
325                 if (desktop != 0xFFFFFFFF)
326                         ret = (gint) desktop;
327         }
328 
329         if (type_return != None && data != NULL)
330                 XFree(data);
331 
332         return ret;
333 }
334 
display_get_desktop(GdkDisplay * d)335 static gint display_get_desktop(GdkDisplay *d) {
336         Atom type_return;
337         gint format_return;
338         gulong nitems_return;
339         gulong bytes_after_return;
340         guchar *data = NULL;
341         gint ret = -1;
342 
343 #ifdef GDK_IS_X11_DISPLAY
344         if (!GDK_IS_X11_DISPLAY(d))
345                 return 0;
346 #endif
347 
348         if (XGetWindowProperty(GDK_DISPLAY_XDISPLAY(d), DefaultRootWindow(GDK_DISPLAY_XDISPLAY(d)),
349                                gdk_x11_get_xatom_by_name_for_display(d, "_NET_CURRENT_DESKTOP"),
350                                0, G_MAXLONG, False, XA_CARDINAL, &type_return,
351                                &format_return, &nitems_return, &bytes_after_return,
352                                &data) != Success)
353                 return -1;
354 
355         if (type_return == XA_CARDINAL && format_return == 32 && data) {
356 
357                 guint32 desktop = *(guint32*) data;
358 
359                 if (desktop != 0xFFFFFFFF)
360                         ret = (gint) desktop;
361         }
362 
363         if (type_return != None && data != NULL)
364                 XFree(data);
365 
366         return ret;
367 }
368 
window_is_xembed(GdkDisplay * d,GdkWindow * w)369 static gboolean window_is_xembed(GdkDisplay *d, GdkWindow *w) {
370         Atom type_return;
371         gint format_return;
372         gulong nitems_return;
373         gulong bytes_after_return;
374         guchar *data = NULL;
375         gboolean ret = FALSE;
376         Atom xembed;
377 
378 #ifdef GDK_IS_X11_DISPLAY
379         if (!GDK_IS_X11_DISPLAY(d))
380                 return FALSE;
381 #endif
382 
383         /* Gnome Panel applets are XEMBED windows. We need to make sure we
384          * ignore them */
385 
386         xembed = gdk_x11_get_xatom_by_name_for_display(d, "_XEMBED_INFO");
387 
388         /* be robust against not existing XIDs (LP: #834403) */
389         gdk_error_trap_push();
390         if (XGetWindowProperty(GDK_DISPLAY_XDISPLAY(d), GDK_WINDOW_XID(w),
391                                xembed,
392                                0, 2, False, xembed, &type_return,
393                                &format_return, &nitems_return, &bytes_after_return,
394                                &data) != Success) {
395                 return FALSE;
396         }
397 
398 #if GTK_CHECK_VERSION(3,0,0)
399         gdk_error_trap_pop_ignored();
400 #else
401         gdk_flush();
402         gdk_error_trap_pop();
403 #endif
404 
405         if (type_return == xembed && format_return == 32 && data)
406                 ret = TRUE;
407 
408         if (type_return != None && data != NULL)
409                 XFree(data);
410 
411         return ret;
412 }
413 
dispatch_sound_event(SoundEventData * d)414 static void dispatch_sound_event(SoundEventData *d) {
415         int ret = CA_SUCCESS;
416         static gboolean menu_is_popped_up = FALSE;
417 
418         if (g_object_get_qdata(d->object, disable_sound_quark))
419                 return;
420 
421         /* The GdkWindow of the the widget might have changed while this
422          * event was queued for us. Make sure to update it from the
423          * current one if necessary. */
424         if (d->event && d->event->any.window) {
425                 GdkWindow *window;
426 
427                 g_object_unref(G_OBJECT(d->event->any.window));
428 
429                 if ((window = gtk_widget_get_window(GTK_WIDGET(d->object))))
430                         d->event->any.window = GDK_WINDOW(g_object_ref(G_OBJECT(window)));
431                 else
432                         d->event->any.window = NULL;
433         }
434 
435         if (d->signal_id == signal_id_widget_show) {
436                 GdkWindowTypeHint hint;
437 
438                 /* Show/hide signals for non-windows have already been filtered out
439                  * by the emission hook! */
440 
441                 hint = gtk_window_get_type_hint(GTK_WINDOW(d->object));
442 
443                 if (is_menu_hint(hint)) {
444 
445                         if (!menu_is_popped_up) {
446 
447                                 ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
448                                                              CA_PROP_EVENT_ID, "menu-popup",
449                                                              CA_PROP_EVENT_DESCRIPTION, "Menu popped up",
450                                                              CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
451                                                              NULL);
452                         } else {
453                                 ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
454                                                              CA_PROP_EVENT_ID, "menu-replace",
455                                                              CA_PROP_EVENT_DESCRIPTION, "Menu replaced",
456                                                              CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
457                                                              NULL);
458                         }
459 
460                         menu_is_popped_up = TRUE;
461 
462                 } else if (hint == GDK_WINDOW_TYPE_HINT_TOOLTIP) {
463 
464                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
465                                                      CA_PROP_EVENT_ID, "tooltip-popup",
466                                                      CA_PROP_EVENT_DESCRIPTION, "Tooltip popped up",
467                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
468                                                      NULL);
469 
470                 } else if (hint == GDK_WINDOW_TYPE_HINT_NORMAL ||
471                            hint == GDK_WINDOW_TYPE_HINT_DIALOG) {
472 
473                         gboolean played_sound = FALSE;
474                         gboolean is_xembed;
475 
476                         is_xembed =
477                                 gtk_widget_get_realized(GTK_WIDGET(d->object)) &&
478                                 window_is_xembed(
479                                                 gtk_widget_get_display(GTK_WIDGET(d->object)),
480                                                 gtk_widget_get_window(GTK_WIDGET(d->object)));
481 
482                         g_object_set_qdata(d->object, is_xembed_quark, GINT_TO_POINTER(is_xembed));
483 
484                         if (GTK_IS_MESSAGE_DIALOG(d->object)) {
485                                 GtkMessageType mt;
486                                 const char *id;
487 
488                                 g_object_get(d->object, "message_type", &mt, NULL);
489 
490                                 if ((id = translate_message_tye(mt))) {
491 
492                                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
493                                                                      CA_PROP_EVENT_ID, id,
494                                                                      CA_PROP_EVENT_DESCRIPTION, "Message dialog shown",
495                                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
496                                                                      NULL);
497                                         played_sound = TRUE;
498                                 }
499 
500                         }
501 
502                         if (!played_sound &&
503                             !is_xembed &&
504                             gtk_window_get_decorated(GTK_WINDOW(d->object))) {
505 
506                                 ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
507                                                              CA_PROP_EVENT_ID, "window-new",
508                                                              CA_PROP_EVENT_DESCRIPTION, "Window shown",
509                                                              CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
510                                                              NULL);
511 
512                         }
513                 }
514         }
515 
516         if (GTK_IS_DIALOG(d->object) && d->signal_id == signal_id_dialog_response) {
517 
518                 int response;
519                 const char *id;
520 
521                 response = g_value_get_int(&d->arg1);
522 
523                 if ((id = translate_response(response))) {
524 
525                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
526                                                      CA_PROP_EVENT_ID, id,
527                                                      CA_PROP_EVENT_DESCRIPTION, "Dialog closed",
528                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
529                                                      NULL);
530                 } else {
531                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
532                                                      CA_PROP_EVENT_ID, "window-close",
533                                                      CA_PROP_EVENT_DESCRIPTION, "Window closed",
534                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
535                                                      NULL);
536                 }
537 
538         } else if (d->signal_id == signal_id_widget_hide) {
539                 GdkWindowTypeHint hint;
540 
541                 hint = gtk_window_get_type_hint(GTK_WINDOW(d->object));
542 
543                 if (is_menu_hint(hint)) {
544 
545                         if (GTK_IS_MENU(gtk_bin_get_child(GTK_BIN(d->object)))) {
546 
547                                 ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
548                                                              CA_PROP_EVENT_ID, "menu-popdown",
549                                                              CA_PROP_EVENT_DESCRIPTION, "Menu popped down",
550                                                              CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
551                                                              NULL);
552                         }
553 
554                         menu_is_popped_up = FALSE;
555 
556                 } else if (hint == GDK_WINDOW_TYPE_HINT_TOOLTIP) {
557 
558                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
559                                                      CA_PROP_EVENT_ID, "tooltip-popdown",
560                                                      CA_PROP_EVENT_DESCRIPTION, "Tooltip popped down",
561                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
562                                                      NULL);
563 
564                 } else if ((hint == GDK_WINDOW_TYPE_HINT_NORMAL ||
565                             hint == GDK_WINDOW_TYPE_HINT_DIALOG)) {
566 
567                         gboolean is_xembed;
568 
569                         is_xembed = !!g_object_get_qdata(d->object, is_xembed_quark);
570 
571                         if (!is_xembed &&
572                             gtk_window_get_decorated(GTK_WINDOW(d->object)))
573                                 ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
574                                                              CA_PROP_EVENT_ID, "window-close",
575                                                              CA_PROP_EVENT_DESCRIPTION, "Window closed",
576                                                              CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
577                                                              NULL);
578                 }
579         }
580 
581         if (GTK_IS_WINDOW(d->object) && d->signal_id == signal_id_widget_window_state_event) {
582                 GdkEventWindowState *e;
583                 gint w_desktop = -1, c_desktop = -1;
584 
585                 e = (GdkEventWindowState*) d->event;
586 
587                 /* Unfortunately GDK_WINDOW_STATE_ICONIFIED is used both for
588                  * proper minimizing and when a window becomes invisible
589                  * because the desktop was switched. To handle this we check
590                  * if the window becoming invisible is actually on the current
591                  * desktop, and only if that's the case we assume it is being
592                  * minimized. We then store this information, so that we know
593                  * later on when the window is unminimized again. */
594 
595                 if (gtk_widget_get_realized(GTK_WIDGET(d->object))) {
596                         GdkDisplay *display;
597 
598                         display = gtk_widget_get_display(GTK_WIDGET(d->object));
599                         w_desktop = window_get_desktop(display, gtk_widget_get_window(GTK_WIDGET(d->object)));
600                         c_desktop = display_get_desktop(display);
601                 }
602 
603                 if ((e->changed_mask & GDK_WINDOW_STATE_ICONIFIED) &&
604                     (e->new_window_state & GDK_WINDOW_STATE_ICONIFIED) &&
605                     (w_desktop == c_desktop || w_desktop < 0)) {
606 
607                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
608                                                      CA_PROP_EVENT_ID, "window-minimized",
609                                                      CA_PROP_EVENT_DESCRIPTION, "Window minimized",
610                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
611                                                      NULL);
612 
613                         g_object_set_qdata(d->object, was_iconized_quark, GINT_TO_POINTER(1));
614 
615                 } else if ((e->changed_mask & (GDK_WINDOW_STATE_MAXIMIZED|GDK_WINDOW_STATE_FULLSCREEN)) &&
616                            (e->new_window_state & (GDK_WINDOW_STATE_MAXIMIZED|GDK_WINDOW_STATE_FULLSCREEN))) {
617 
618                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
619                                                      CA_PROP_EVENT_ID, "window-maximized",
620                                                      CA_PROP_EVENT_DESCRIPTION, "Window maximized",
621                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
622                                                      NULL);
623 
624                         g_object_set_qdata(d->object, was_iconized_quark, GINT_TO_POINTER(0));
625 
626                 } else if ((e->changed_mask & GDK_WINDOW_STATE_ICONIFIED) &&
627                            !(e->new_window_state & GDK_WINDOW_STATE_ICONIFIED) &&
628                            g_object_get_qdata(d->object, was_iconized_quark)) {
629 
630                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
631                                                      CA_PROP_EVENT_ID, "window-unminimized",
632                                                      CA_PROP_EVENT_DESCRIPTION, "Window unminimized",
633                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
634                                                      NULL);
635 
636                         g_object_set_qdata(d->object, was_iconized_quark, GINT_TO_POINTER(0));
637 
638                 } else if ((e->changed_mask & (GDK_WINDOW_STATE_MAXIMIZED|GDK_WINDOW_STATE_FULLSCREEN)) &&
639                            !(e->new_window_state & (GDK_WINDOW_STATE_MAXIMIZED|GDK_WINDOW_STATE_FULLSCREEN))) {
640 
641                         ret = ca_gtk_play_for_widget(GTK_WIDGET(d->object), 0,
642                                                      CA_PROP_EVENT_ID, "window-unmaximized",
643                                                      CA_PROP_EVENT_DESCRIPTION, "Window unmaximized",
644                                                      CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
645                                                      NULL);
646                 }
647         }
648 
649         if (GTK_IS_CHECK_MENU_ITEM(d->object) && d->signal_id == signal_id_check_menu_item_toggled) {
650 
651                 if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(d->object)))
652                         ret = ca_gtk_play_for_event(d->event, 0,
653                                                     CA_PROP_EVENT_ID, "button-toggle-on",
654                                                     CA_PROP_EVENT_DESCRIPTION, "Check menu item checked",
655                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
656                                                     NULL);
657                 else
658                         ret = ca_gtk_play_for_event(d->event, 0,
659                                                     CA_PROP_EVENT_ID, "button-toggle-off",
660                                                     CA_PROP_EVENT_DESCRIPTION, "Check menu item unchecked",
661                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
662                                                     NULL);
663 
664         } else if (GTK_IS_MENU_ITEM(d->object) && d->signal_id == signal_id_menu_item_activate) {
665 
666                 if (!gtk_menu_item_get_submenu(GTK_MENU_ITEM(d->object)))
667                         ret = ca_gtk_play_for_event(d->event, 0,
668                                                     CA_PROP_EVENT_ID, "menu-click",
669                                                     CA_PROP_EVENT_DESCRIPTION, "Menu item clicked",
670                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
671                                                     NULL);
672         }
673 
674         if (GTK_IS_TOGGLE_BUTTON(d->object)) {
675 
676                 if (d->signal_id == signal_id_toggle_button_toggled) {
677 
678                         if (!is_child_of_combo_box(GTK_WIDGET(d->object))) {
679 
680                                 /* We don't want to play this sound if this is a toggle
681                                  * button belonging to combo box. */
682 
683                                 if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->object)))
684                                         ret = ca_gtk_play_for_event(d->event, 0,
685                                                                     CA_PROP_EVENT_ID, "button-toggle-on",
686                                                                     CA_PROP_EVENT_DESCRIPTION, "Toggle button checked",
687                                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
688                                                                     NULL);
689                                 else
690                                         ret = ca_gtk_play_for_event(d->event, 0,
691                                                                     CA_PROP_EVENT_ID, "button-toggle-off",
692                                                                     CA_PROP_EVENT_DESCRIPTION, "Toggle button unchecked",
693                                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
694                                                                     NULL);
695                         }
696                 }
697 
698         } else if (GTK_IS_LINK_BUTTON(d->object)) {
699 
700                 if (d->signal_id == signal_id_button_pressed) {
701                         ret = ca_gtk_play_for_event(d->event, 0,
702                                                     CA_PROP_EVENT_ID, "link-pressed",
703                                                     CA_PROP_EVENT_DESCRIPTION, "Link pressed",
704                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
705                                                     NULL);
706 
707                 } else if (d->signal_id == signal_id_button_released) {
708 
709                         ret = ca_gtk_play_for_event(d->event, 0,
710                                                     CA_PROP_EVENT_ID, "link-released",
711                                                     CA_PROP_EVENT_DESCRIPTION, "Link released",
712                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
713                                                     NULL);
714                 }
715 
716         } else if (GTK_IS_BUTTON(d->object) && !GTK_IS_TOGGLE_BUTTON(d->object)) {
717 
718                 if (d->signal_id == signal_id_button_pressed) {
719                         ret = ca_gtk_play_for_event(d->event, 0,
720                                                     CA_PROP_EVENT_ID, "button-pressed",
721                                                     CA_PROP_EVENT_DESCRIPTION, "Button pressed",
722                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
723                                                     NULL);
724 
725                 } else if (d->signal_id == signal_id_button_released) {
726                         GtkDialog *dialog;
727                         gboolean dont_play = FALSE;
728 
729                         if ((dialog = find_parent_dialog(GTK_WIDGET(d->object)))) {
730                                 int response;
731 
732                                 /* Don't play the click sound if this is a response widget
733                                  * we will generate a dialog-xxx event sound anyway. */
734 
735                                 response = gtk_dialog_get_response_for_widget(dialog, GTK_WIDGET(d->object));
736                                 dont_play = !!translate_response(response);
737                         }
738 
739                         if (!dont_play)
740                                 ret = ca_gtk_play_for_event(d->event, 0,
741                                                             CA_PROP_EVENT_ID, "button-released",
742                                                             CA_PROP_EVENT_DESCRIPTION, "Button released",
743                                                             CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
744                                                             NULL);
745                 }
746         }
747 
748         if (GTK_IS_NOTEBOOK(d->object) && d->signal_id == signal_id_notebook_switch_page) {
749                 ret = ca_gtk_play_for_event(d->event, 0,
750                                             CA_PROP_EVENT_ID, "notebook-tab-changed",
751                                             CA_PROP_EVENT_DESCRIPTION, "Tab changed",
752                                             CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
753                                             NULL);
754                 goto finish;
755         }
756 
757         if (GTK_IS_TREE_VIEW(d->object) && d->signal_id == signal_id_tree_view_cursor_changed) {
758                 ret = ca_gtk_play_for_event(d->event, 0,
759                                             CA_PROP_EVENT_ID, "item-selected",
760                                             CA_PROP_EVENT_DESCRIPTION, "Item selected",
761                                             CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
762                                             NULL);
763                 goto finish;
764         }
765 
766         if (GTK_IS_ICON_VIEW(d->object) && d->signal_id == signal_id_icon_view_selection_changed) {
767                 ret = ca_gtk_play_for_event(d->event, 0,
768                                             CA_PROP_EVENT_ID, "item-selected",
769                                             CA_PROP_EVENT_DESCRIPTION, "Item selected",
770                                             CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
771                                             NULL);
772                 goto finish;
773         }
774 
775         if (GTK_IS_EXPANDER(d->object) && d->signal_id == signal_id_expander_activate) {
776 
777                 if (gtk_expander_get_expanded(GTK_EXPANDER(d->object)))
778                         ret = ca_gtk_play_for_event(d->event, 0,
779                                                     CA_PROP_EVENT_ID, "expander-toggle-on",
780                                                     CA_PROP_EVENT_DESCRIPTION, "Expander expanded",
781                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
782                                                     NULL);
783                 else
784                         ret = ca_gtk_play_for_event(d->event, 0,
785                                                     CA_PROP_EVENT_ID, "expander-toggle-off",
786                                                     CA_PROP_EVENT_DESCRIPTION, "Expander unexpanded",
787                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
788                                                     NULL);
789 
790                 goto finish;
791         }
792 
793         if (GTK_IS_WIDGET(d->object)) {
794 
795                 if (d->signal_id == signal_id_widget_drag_begin) {
796 
797                         ret = ca_gtk_play_for_event(d->event, 0,
798                                                     CA_PROP_EVENT_ID, "drag-start",
799                                                     CA_PROP_EVENT_DESCRIPTION, "Drag started",
800                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
801                                                     NULL);
802                         goto finish;
803 
804                 } else if (d->signal_id == signal_id_widget_drag_drop) {
805 
806                         ret = ca_gtk_play_for_event(d->event, 0,
807                                                     CA_PROP_EVENT_ID, "drag-accept",
808                                                     CA_PROP_EVENT_DESCRIPTION, "Drag accepted",
809                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
810                                                     NULL);
811                         goto finish;
812 
813                 } else if (d->signal_id == signal_id_widget_drag_failed) {
814 
815                         ret = ca_gtk_play_for_event(d->event, 0,
816                                                     CA_PROP_EVENT_ID, "drag-fail",
817                                                     CA_PROP_EVENT_DESCRIPTION, "Drag failed",
818                                                     CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
819                                                     NULL);
820                         goto finish;
821                 }
822         }
823 
824 finish:
825 
826         ;
827         /* if (ret != CA_SUCCESS) */
828         /*     g_warning("Failed to play event sound: %s", ca_strerror(ret)); */
829 }
830 
dispatch_queue(void)831 static void dispatch_queue(void) {
832         SoundEventData *d;
833 
834         while ((d = g_queue_pop_head(&sound_event_queue))) {
835 
836                 if (!(d = filter_sound_event(d)))
837                         continue;
838 
839                 dispatch_sound_event(d);
840                 free_sound_event(d);
841         }
842 }
843 
idle_cb(void * userdata)844 static gboolean idle_cb(void *userdata) {
845         idle_id = 0;
846 
847         dispatch_queue();
848 
849         return FALSE;
850 }
851 
852 static void connect_settings(void);
853 
emission_hook_cb(GSignalInvocationHint * hint,guint n_param_values,const GValue * param_values,gpointer data)854 static gboolean emission_hook_cb(GSignalInvocationHint *hint, guint n_param_values, const GValue *param_values, gpointer data) {
855         static SoundEventData *d = NULL;
856         GdkEvent *e;
857         GObject *object;
858 
859         connect_settings();
860 
861         if (disabled)
862                 return TRUE;
863 
864         object = g_value_get_object(&param_values[0]);
865 
866         /* g_message("signal '%s' on object of type '%s' with name '%s'", */
867         /*           g_signal_name(hint->signal_id), */
868         /*           G_OBJECT_TYPE_NAME(object), */
869         /*           gtk_widget_get_name(GTK_WIDGET(object))); */
870 
871         /* if (GTK_IS_WINDOW(object)) */
872         /*     g_message("window role='%s' title='%s' type='%u'", */
873         /*               gtk_window_get_role(GTK_WINDOW(object)), */
874         /*               gtk_window_get_title(GTK_WINDOW(object)), */
875         /*               gtk_window_get_type_hint(GTK_WINDOW(object))); */
876 
877         /* Filter a few very often occuring signals as quickly as possible */
878         if ((hint->signal_id == signal_id_widget_hide ||
879              hint->signal_id == signal_id_widget_show ||
880              hint->signal_id == signal_id_widget_window_state_event) &&
881             !GTK_IS_WINDOW(object))
882                 return TRUE;
883 
884         if (hint->signal_id != signal_id_widget_hide &&
885             hint->signal_id != signal_id_dialog_response &&
886             !gtk_widget_is_drawable(GTK_WIDGET (object)))
887                 return TRUE;
888 
889         d = g_slice_new0(SoundEventData);
890 
891         d->object = g_object_ref(object);
892 
893         d->signal_id = hint->signal_id;
894 
895         if (d->signal_id == signal_id_widget_window_state_event) {
896                 d->event = gdk_event_copy(g_value_peek_pointer(&param_values[1]));
897         } else if ((e = gtk_get_current_event()))
898                 d->event = gdk_event_copy(e);
899 
900         if (n_param_values > 1) {
901                 g_value_init(&d->arg1, G_VALUE_TYPE(&param_values[1]));
902                 g_value_copy(&param_values[1], &d->arg1);
903                 d->arg1_is_set = TRUE;
904         }
905 
906         g_queue_push_tail(&sound_event_queue, d);
907 
908         if (idle_id == 0)
909                 idle_id = gdk_threads_add_idle_full(GDK_PRIORITY_REDRAW-1, (GSourceFunc) idle_cb, NULL, NULL);
910 
911         return TRUE;
912 }
913 
install_hook(GType type,const char * sig,guint * sn)914 static void install_hook(GType type, const char *sig, guint *sn) {
915         GTypeClass *type_class;
916 
917         type_class = g_type_class_ref(type);
918 
919         *sn = g_signal_lookup(sig, type);
920         g_signal_add_emission_hook(*sn, 0, emission_hook_cb, NULL, NULL);
921 
922         g_type_class_unref(type_class);
923 }
924 
read_enable_input_feedback_sounds(GtkSettings * s)925 static void read_enable_input_feedback_sounds(GtkSettings *s) {
926         gboolean enabled = !disabled;
927 
928         if (g_getenv("CANBERRA_FORCE_INPUT_FEEDBACK_SOUNDS"))
929                 disabled = FALSE;
930         else {
931                 g_object_get(G_OBJECT(s), "gtk-enable-input-feedback-sounds", &enabled, NULL);
932                 disabled = !enabled;
933         }
934 }
935 
enable_input_feedback_sounds_changed(GtkSettings * s,GParamSpec * arg1,gpointer userdata)936 static void enable_input_feedback_sounds_changed(GtkSettings *s, GParamSpec *arg1, gpointer userdata) {
937         read_enable_input_feedback_sounds(s);
938 }
939 
connect_settings(void)940 static void connect_settings(void) {
941         GtkSettings *s;
942         static gboolean connected = FALSE;
943 
944         if (connected)
945                 return;
946 
947         if (!(s = gtk_settings_get_default()))
948                 return;
949 
950         if (g_object_class_find_property(G_OBJECT_GET_CLASS(s), "gtk-enable-input-feedback-sounds")) {
951                 g_signal_connect(G_OBJECT(s), "notify::gtk-enable-input-feedback-sounds", G_CALLBACK(enable_input_feedback_sounds_changed), NULL);
952                 read_enable_input_feedback_sounds(s);
953         } else
954                 g_debug("This Gtk+ version doesn't have the GtkSettings::gtk-enable-input-feedback-sounds property.");
955 
956         connected = TRUE;
957 }
958 
959 #if GTK_CHECK_VERSION(3,0,0)
960 #warning "We really need a quit handler in Gtk 3.0, https://bugzilla.gnome.org/show_bug.cgi?id=639770"
961 #else
quit_handler(gpointer data)962 static gboolean quit_handler(gpointer data) {
963         dispatch_queue();
964         return FALSE;
965 }
966 #endif
967 
gtk_module_init(gint * argc,gchar *** argv[])968 G_MODULE_EXPORT void gtk_module_init(gint *argc, gchar ***argv[]) {
969 
970         /* This is the same quark libgnomeui uses! */
971         disable_sound_quark = g_quark_from_string("gnome_disable_sound_events");
972         was_iconized_quark = g_quark_from_string("canberra_was_iconized");
973         is_xembed_quark = g_quark_from_string("canberra_is_xembed");
974 
975         /* Hook up the gtk setting */
976         connect_settings();
977 
978         install_hook(GTK_TYPE_WINDOW, "show", &signal_id_widget_show);
979         install_hook(GTK_TYPE_WINDOW, "hide", &signal_id_widget_hide);
980         install_hook(GTK_TYPE_DIALOG, "response", &signal_id_dialog_response);
981         install_hook(GTK_TYPE_MENU_ITEM, "activate", &signal_id_menu_item_activate);
982         install_hook(GTK_TYPE_CHECK_MENU_ITEM, "toggled", &signal_id_check_menu_item_toggled);
983         install_hook(GTK_TYPE_TOGGLE_BUTTON, "toggled", &signal_id_toggle_button_toggled);
984         install_hook(GTK_TYPE_BUTTON, "pressed", &signal_id_button_pressed);
985         install_hook(GTK_TYPE_BUTTON, "released", &signal_id_button_released);
986         install_hook(GTK_TYPE_WIDGET, "window-state-event", &signal_id_widget_window_state_event);
987         install_hook(GTK_TYPE_NOTEBOOK, "switch-page", &signal_id_notebook_switch_page);
988         install_hook(GTK_TYPE_TREE_VIEW, "cursor-changed", &signal_id_tree_view_cursor_changed);
989         install_hook(GTK_TYPE_ICON_VIEW, "selection-changed", &signal_id_icon_view_selection_changed);
990         install_hook(GTK_TYPE_WIDGET, "drag-begin", &signal_id_widget_drag_begin);
991         install_hook(GTK_TYPE_WIDGET, "drag-drop", &signal_id_widget_drag_drop);
992         install_hook(GTK_TYPE_WIDGET, "drag-failed", &signal_id_widget_drag_failed);
993         install_hook(GTK_TYPE_EXPANDER, "activate", &signal_id_expander_activate);
994 
995 #if !GTK_CHECK_VERSION(3,0,0)
996         gtk_quit_add(1, quit_handler, NULL);
997 #endif
998 }
999 
1000 G_MODULE_EXPORT gchar* g_module_check_init(GModule *module);
1001 
g_module_check_init(GModule * module)1002 G_MODULE_EXPORT gchar* g_module_check_init(GModule *module) {
1003         g_module_make_resident(module);
1004         return NULL;
1005 }
1006