1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
2  *
3  * Copyright (C) 2008 Bastien Nocera <hadess@hadess.net>
4  * Copyright (C) 2008 William Jon McCann
5  * Copyright (C) 2014-2021 MATE Developers
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "config.h"
24 
25 #include <stdlib.h>
26 #include <stdio.h>
27 #include <unistd.h>
28 #include <utime.h>
29 #include <errno.h>
30 
31 #include <glib.h>
32 #include <glib/gi18n.h>
33 #include <glib-object.h>
34 #include <gio/gio.h>
35 #include <gtk/gtk.h>
36 #include <canberra-gtk.h>
37 #include <libxml/tree.h>
38 
39 #include "gvc-sound-theme-chooser.h"
40 #include "sound-theme-file-utils.h"
41 
42 struct GvcSoundThemeChooserPrivate
43 {
44         GtkWidget *combo_box;
45         GtkWidget *treeview;
46         GtkWidget *theme_box;
47         GtkWidget *selection_box;
48         GtkWidget *click_feedback_button;
49         GSettings *sound_settings;
50 };
51 
52 static void     gvc_sound_theme_chooser_dispose   (GObject            *object);
53 
54 G_DEFINE_TYPE_WITH_PRIVATE (GvcSoundThemeChooser, gvc_sound_theme_chooser, GTK_TYPE_BOX)
55 
56 #define KEY_SOUNDS_SCHEMA          "org.mate.sound"
57 #define EVENT_SOUNDS_KEY           "event-sounds"
58 #define INPUT_SOUNDS_KEY           "input-feedback-sounds"
59 #define SOUND_THEME_KEY            "theme-name"
60 
61 #define DEFAULT_ALERT_ID        "__default"
62 #define CUSTOM_THEME_NAME       "__custom"
63 #define NO_SOUNDS_THEME_NAME    "__no_sounds"
64 
65 enum {
66         THEME_DISPLAY_COL,
67         THEME_IDENTIFIER_COL,
68         THEME_PARENT_ID_COL,
69         THEME_NUM_COLS
70 };
71 
72 enum {
73         ALERT_DISPLAY_COL,
74         ALERT_IDENTIFIER_COL,
75         ALERT_SOUND_TYPE_COL,
76         ALERT_ACTIVE_COL,
77         ALERT_NUM_COLS
78 };
79 
80 enum {
81         SOUND_TYPE_UNSET,
82         SOUND_TYPE_OFF,
83         SOUND_TYPE_DEFAULT_FROM_THEME,
84         SOUND_TYPE_BUILTIN,
85         SOUND_TYPE_CUSTOM
86 };
87 
88 static void
on_combobox_changed(GtkComboBox * widget,GvcSoundThemeChooser * chooser)89 on_combobox_changed (GtkComboBox          *widget,
90                      GvcSoundThemeChooser *chooser)
91 {
92         GtkTreeIter   iter;
93         GtkTreeModel *model;
94         char         *theme_name;
95 
96         if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (chooser->priv->combo_box), &iter) == FALSE) {
97                 return;
98         }
99 
100         model = gtk_combo_box_get_model (GTK_COMBO_BOX (chooser->priv->combo_box));
101         gtk_tree_model_get (model, &iter, THEME_IDENTIFIER_COL, &theme_name, -1);
102 
103         g_assert (theme_name != NULL);
104 
105         /* It is necessary to update the theme name before any other setting as
106          * the "changed" notification will reload the contents of the widget */
107         g_settings_set_string (chooser->priv->sound_settings, SOUND_THEME_KEY, theme_name);
108 
109         /* special case for no sounds */
110         if (strcmp (theme_name, NO_SOUNDS_THEME_NAME) == 0) {
111                 g_settings_set_boolean (chooser->priv->sound_settings, EVENT_SOUNDS_KEY, FALSE);
112                 return;
113         } else {
114                 g_settings_set_boolean (chooser->priv->sound_settings, EVENT_SOUNDS_KEY, TRUE);
115         }
116 
117         g_free (theme_name);
118 
119         /* FIXME: reset alert model */
120 }
121 
122 static char *
load_index_theme_name(const char * index,char ** parent)123 load_index_theme_name (const char *index,
124                        char      **parent)
125 {
126         GKeyFile *file;
127         char *indexname = NULL;
128         gboolean hidden;
129 
130         file = g_key_file_new ();
131         if (g_key_file_load_from_file (file, index, G_KEY_FILE_KEEP_TRANSLATIONS, NULL) == FALSE) {
132                 g_key_file_free (file);
133                 return NULL;
134         }
135         /* Don't add hidden themes to the list */
136         hidden = g_key_file_get_boolean (file, "Sound Theme", "Hidden", NULL);
137         if (!hidden) {
138                 indexname = g_key_file_get_locale_string (file,
139                                                           "Sound Theme",
140                                                           "Name",
141                                                           NULL,
142                                                           NULL);
143 
144                 /* Save the parent theme, if there's one */
145                 if (parent != NULL) {
146                         *parent = g_key_file_get_string (file,
147                                                          "Sound Theme",
148                                                          "Inherits",
149                                                          NULL);
150                 }
151         }
152 
153         g_key_file_free (file);
154         return indexname;
155 }
156 
157 static void
sound_theme_in_dir(GHashTable * hash,const char * dir)158 sound_theme_in_dir (GHashTable *hash,
159                     const char *dir)
160 {
161         GDir *d;
162         const char *name;
163 
164         d = g_dir_open (dir, 0, NULL);
165         if (d == NULL) {
166                 return;
167         }
168 
169         while ((name = g_dir_read_name (d)) != NULL) {
170                 char *dirname, *index, *indexname;
171 
172                 /* Look for directories */
173                 dirname = g_build_filename (dir, name, NULL);
174                 if (g_file_test (dirname, G_FILE_TEST_IS_DIR) == FALSE) {
175                         g_free (dirname);
176                         continue;
177                 }
178 
179                 /* Look for index files */
180                 index = g_build_filename (dirname, "index.theme", NULL);
181                 g_free (dirname);
182 
183                 /* Check the name of the theme in the index.theme file */
184                 indexname = load_index_theme_name (index, NULL);
185                 g_free (index);
186                 if (indexname == NULL) {
187                         continue;
188                 }
189 
190                 g_hash_table_insert (hash, g_strdup (name), indexname);
191         }
192 
193         g_dir_close (d);
194 }
195 
196 static void
add_theme_to_store(const char * key,const char * value,GtkListStore * store)197 add_theme_to_store (const char   *key,
198                     const char   *value,
199                     GtkListStore *store)
200 {
201         char *parent;
202 
203         parent = NULL;
204 
205         /* Get the parent, if we're checking the custom theme */
206         if (strcmp (key, CUSTOM_THEME_NAME) == 0) {
207                 char *name, *path;
208 
209                 path = custom_theme_dir_path ("index.theme");
210                 name = load_index_theme_name (path, &parent);
211                 g_free (name);
212                 g_free (path);
213         }
214         gtk_list_store_insert_with_values (store, NULL, G_MAXINT,
215                                            THEME_DISPLAY_COL, value,
216                                            THEME_IDENTIFIER_COL, key,
217                                            THEME_PARENT_ID_COL, parent,
218                                            -1);
219         g_free (parent);
220 }
221 
222 static void
set_combox_for_theme_name(GvcSoundThemeChooser * chooser,const char * name)223 set_combox_for_theme_name (GvcSoundThemeChooser *chooser,
224                            const char           *name)
225 {
226         GtkTreeIter   iter;
227         GtkTreeModel *model;
228         gboolean      found;
229 
230         /* If the name is empty, use "freedesktop" */
231         if (name == NULL || *name == '\0') {
232                 name = "freedesktop";
233         }
234 
235         model = gtk_combo_box_get_model (GTK_COMBO_BOX (chooser->priv->combo_box));
236 
237         if (gtk_tree_model_get_iter_first (model, &iter) == FALSE) {
238                 return;
239         }
240 
241         do {
242                 char *value;
243 
244                 gtk_tree_model_get (model, &iter, THEME_IDENTIFIER_COL, &value, -1);
245                 found = (value != NULL && strcmp (value, name) == 0);
246                 g_free (value);
247 
248         } while (!found && gtk_tree_model_iter_next (model, &iter));
249 
250         /* When we can't find the theme we need to set, try to set the default
251          * one "freedesktop" */
252         if (found) {
253                 gtk_combo_box_set_active_iter (GTK_COMBO_BOX (chooser->priv->combo_box), &iter);
254         } else if (strcmp (name, "freedesktop") != 0) {
255                 g_debug ("not found, falling back to fdo");
256                 set_combox_for_theme_name (chooser, "freedesktop");
257         }
258 }
259 
260 static void
set_input_feedback_enabled(GvcSoundThemeChooser * chooser,gboolean enabled)261 set_input_feedback_enabled (GvcSoundThemeChooser *chooser,
262                             gboolean              enabled)
263 {
264         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (chooser->priv->click_feedback_button),
265                                       enabled);
266 }
267 
268 static void
setup_theme_selector(GvcSoundThemeChooser * chooser)269 setup_theme_selector (GvcSoundThemeChooser *chooser)
270 {
271         GHashTable           *hash;
272         GtkListStore         *store;
273         GtkCellRenderer      *renderer;
274         const char * const   *data_dirs;
275         const char           *data_dir;
276         char                 *dir;
277         guint                 i;
278 
279         /* Add the theme names and their display name to a hash table,
280          * makes it easy to avoid duplicate themes */
281         hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
282 
283         data_dirs = g_get_system_data_dirs ();
284         for (i = 0; data_dirs[i] != NULL; i++) {
285                 dir = g_build_filename (data_dirs[i], "sounds", NULL);
286                 sound_theme_in_dir (hash, dir);
287                 g_free (dir);
288         }
289 
290         data_dir = g_get_user_data_dir ();
291         dir = g_build_filename (data_dir, "sounds", NULL);
292         sound_theme_in_dir (hash, dir);
293         g_free (dir);
294 
295         /* If there isn't at least one theme, make everything
296          * insensitive, LAME! */
297         if (g_hash_table_size (hash) == 0) {
298                 gtk_widget_set_sensitive (GTK_WIDGET (chooser), FALSE);
299                 g_warning ("Bad setup, install the freedesktop sound theme");
300                 g_hash_table_destroy (hash);
301                 return;
302         }
303 
304         /* Setup the tree model, 3 columns:
305          * - internal theme name/directory
306          * - display theme name
307          * - the internal id for the parent theme, used for the custom theme */
308         store = gtk_list_store_new (THEME_NUM_COLS,
309                                     G_TYPE_STRING,
310                                     G_TYPE_STRING,
311                                     G_TYPE_STRING);
312 
313         /* Add the themes to a combobox */
314         gtk_list_store_insert_with_values (store,
315                                            NULL,
316                                            G_MAXINT,
317                                            THEME_DISPLAY_COL, _("No sounds"),
318                                            THEME_IDENTIFIER_COL, "__no_sounds",
319                                            THEME_PARENT_ID_COL, NULL,
320                                            -1);
321         g_hash_table_foreach (hash, (GHFunc) add_theme_to_store, store);
322         g_hash_table_destroy (hash);
323 
324         /* Set the display */
325         gtk_combo_box_set_model (GTK_COMBO_BOX (chooser->priv->combo_box),
326                                  GTK_TREE_MODEL (store));
327 
328         renderer = gtk_cell_renderer_text_new ();
329         gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (chooser->priv->combo_box),
330                                     renderer,
331                                     TRUE);
332         gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (chooser->priv->combo_box),
333                                         renderer,
334                                         "text", THEME_DISPLAY_COL,
335                                         NULL);
336 
337         g_signal_connect (G_OBJECT (chooser->priv->combo_box),
338                           "changed",
339                           G_CALLBACK (on_combobox_changed),
340                           chooser);
341 }
342 
343 #define GVC_SOUND_SOUND    (xmlChar *) "sound"
344 #define GVC_SOUND_NAME     (xmlChar *) "name"
345 #define GVC_SOUND_FILENAME (xmlChar *) "filename"
346 
347 /* Adapted from yelp-toc-pager.c */
348 static xmlChar *
xml_get_and_trim_names(xmlNodePtr node)349 xml_get_and_trim_names (xmlNodePtr node)
350 {
351         xmlNodePtr cur;
352         xmlChar *keep_lang = NULL;
353         xmlChar *value;
354         int j, keep_pri = INT_MAX;
355 
356         const gchar * const * langs = g_get_language_names ();
357 
358         value = NULL;
359 
360         for (cur = node->children; cur; cur = cur->next) {
361                 if (! xmlStrcmp (cur->name, GVC_SOUND_NAME)) {
362                         xmlChar *cur_lang = NULL;
363                         int cur_pri = INT_MAX;
364 
365                         cur_lang = xmlNodeGetLang (cur);
366 
367                         if (cur_lang) {
368                                 for (j = 0; langs[j]; j++) {
369                                         if (g_str_equal (cur_lang, langs[j])) {
370                                                 cur_pri = j;
371                                                 break;
372                                         }
373                                 }
374                         } else {
375                                 cur_pri = INT_MAX - 1;
376                         }
377 
378                         if (cur_pri <= keep_pri) {
379                                 if (keep_lang)
380                                         xmlFree (keep_lang);
381                                 if (value)
382                                         xmlFree (value);
383 
384                                 value = xmlNodeGetContent (cur);
385 
386                                 keep_lang = cur_lang;
387                                 keep_pri = cur_pri;
388                         } else {
389                                 if (cur_lang)
390                                         xmlFree (cur_lang);
391                         }
392                 }
393         }
394 
395         /* Delete all GVC_SOUND_NAME nodes */
396         cur = node->children;
397         while (cur) {
398                 xmlNodePtr this = cur;
399                 cur = cur->next;
400                 if (! xmlStrcmp (this->name, GVC_SOUND_NAME)) {
401                         xmlUnlinkNode (this);
402                         xmlFreeNode (this);
403                 }
404         }
405 
406         return value;
407 }
408 
409 static void
populate_model_from_node(GvcSoundThemeChooser * chooser,GtkTreeModel * model,xmlNodePtr node)410 populate_model_from_node (GvcSoundThemeChooser *chooser,
411                           GtkTreeModel         *model,
412                           xmlNodePtr            node)
413 {
414         xmlNodePtr child;
415         xmlChar   *filename;
416         xmlChar   *name;
417 
418         filename = NULL;
419         name = xml_get_and_trim_names (node);
420         for (child = node->children; child; child = child->next) {
421                 if (xmlNodeIsText (child)) {
422                         continue;
423                 }
424 
425                 if (xmlStrcmp (child->name, GVC_SOUND_FILENAME) == 0) {
426                         filename = xmlNodeGetContent (child);
427                 } else if (xmlStrcmp (child->name, GVC_SOUND_NAME) == 0) {
428                         /* EH? should have been trimmed */
429                 }
430         }
431 
432         if (filename != NULL && name != NULL) {
433                 gtk_list_store_insert_with_values (GTK_LIST_STORE (model),
434                                                    NULL,
435                                                    G_MAXINT,
436                                                    ALERT_IDENTIFIER_COL, filename,
437                                                    ALERT_DISPLAY_COL, name,
438                                                    ALERT_SOUND_TYPE_COL, _("Built-in"),
439                                                    ALERT_ACTIVE_COL, FALSE,
440                                                    -1);
441         }
442 
443         xmlFree (filename);
444         xmlFree (name);
445 }
446 
447 static void
populate_model_from_file(GvcSoundThemeChooser * chooser,GtkTreeModel * model,const char * filename)448 populate_model_from_file (GvcSoundThemeChooser *chooser,
449                           GtkTreeModel         *model,
450                           const char           *filename)
451 {
452         xmlDocPtr  doc;
453         xmlNodePtr root;
454         xmlNodePtr child;
455         gboolean   exists;
456 
457         exists = g_file_test (filename, G_FILE_TEST_EXISTS);
458         if (! exists) {
459                 return;
460         }
461 
462         doc = xmlParseFile (filename);
463         if (doc == NULL) {
464                 return;
465         }
466 
467         root = xmlDocGetRootElement (doc);
468 
469         for (child = root->children; child; child = child->next) {
470                 if (xmlNodeIsText (child)) {
471                         continue;
472                 }
473                 if (xmlStrcmp (child->name, GVC_SOUND_SOUND) != 0) {
474                         continue;
475                 }
476 
477                 populate_model_from_node (chooser, model, child);
478         }
479 
480         xmlFreeDoc (doc);
481 }
482 
483 static void
populate_model_from_dir(GvcSoundThemeChooser * chooser,GtkTreeModel * model,const char * dirname)484 populate_model_from_dir (GvcSoundThemeChooser *chooser,
485                          GtkTreeModel         *model,
486                          const char           *dirname)
487 {
488         GDir       *d;
489         const char *name;
490 
491         d = g_dir_open (dirname, 0, NULL);
492         if (d == NULL) {
493                 return;
494         }
495 
496         while ((name = g_dir_read_name (d)) != NULL) {
497                 char *path;
498 
499                 if (! g_str_has_suffix (name, ".xml")) {
500                         continue;
501                 }
502 
503                 path = g_build_filename (dirname, name, NULL);
504                 populate_model_from_file (chooser, model, path);
505                 g_free (path);
506         }
507 
508         g_dir_close (d);
509 }
510 
511 static gboolean
save_alert_sounds(GvcSoundThemeChooser * chooser,const char * id)512 save_alert_sounds (GvcSoundThemeChooser  *chooser,
513                    const char            *id)
514 {
515         const char *sounds[3] = { "bell-terminal", "bell-window-system", NULL };
516         char *path;
517 
518         if (strcmp (id, DEFAULT_ALERT_ID) == 0) {
519                 delete_old_files (sounds);
520                 delete_disabled_files (sounds);
521         } else {
522                 delete_old_files (sounds);
523                 delete_disabled_files (sounds);
524                 add_custom_file (sounds, id);
525         }
526 
527         /* And poke the directory so the theme gets updated */
528         path = custom_theme_dir_path (NULL);
529         if (utime (path, NULL) != 0) {
530                 g_warning ("Failed to update mtime for directory '%s': %s",
531                            path, g_strerror (errno));
532         }
533         g_free (path);
534 
535         return FALSE;
536 }
537 
538 
539 static void
update_alert_model(GvcSoundThemeChooser * chooser,const char * id)540 update_alert_model (GvcSoundThemeChooser  *chooser,
541                     const char            *id)
542 {
543         GtkTreeModel *model;
544         GtkTreeIter   iter;
545 
546         model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser->priv->treeview));
547         gtk_tree_model_get_iter_first (model, &iter);
548         do {
549                 gboolean toggled;
550                 char    *this_id;
551 
552                 gtk_tree_model_get (model, &iter,
553                                     ALERT_IDENTIFIER_COL, &this_id,
554                                     -1);
555 
556                 if (strcmp (this_id, id) == 0) {
557                         toggled = TRUE;
558                 } else {
559                         toggled = FALSE;
560                 }
561                 g_free (this_id);
562 
563                 gtk_list_store_set (GTK_LIST_STORE (model),
564                                     &iter,
565                                     ALERT_ACTIVE_COL, toggled,
566                                     -1);
567         } while (gtk_tree_model_iter_next (model, &iter));
568 }
569 
570 static void
update_alert(GvcSoundThemeChooser * chooser,const char * alert_id)571 update_alert (GvcSoundThemeChooser *chooser,
572               const char           *alert_id)
573 {
574         GtkTreeModel *theme_model;
575         GtkTreeIter   iter;
576         char         *theme;
577         char         *parent;
578         gboolean      is_custom;
579         gboolean      is_default;
580         gboolean      add_custom;
581         gboolean      remove_custom;
582 
583         theme_model = gtk_combo_box_get_model (GTK_COMBO_BOX (chooser->priv->combo_box));
584         /* Get the current theme's name, and set the parent */
585         if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (chooser->priv->combo_box), &iter) == FALSE) {
586                 return;
587         }
588 
589         gtk_tree_model_get (theme_model, &iter,
590                             THEME_IDENTIFIER_COL, &theme,
591                             THEME_IDENTIFIER_COL, &parent,
592                             -1);
593         is_custom = strcmp (theme, CUSTOM_THEME_NAME) == 0;
594         is_default = strcmp (alert_id, DEFAULT_ALERT_ID) == 0;
595 
596         /* So a few possibilities:
597          * 1. Named theme, default alert selected: noop
598          * 2. Named theme, alternate alert selected: create new custom with sound
599          * 3. Custom theme, default alert selected: remove sound and possibly custom
600          * 4. Custom theme, alternate alert selected: update custom sound
601          */
602         add_custom = FALSE;
603         remove_custom = FALSE;
604         if (! is_custom && is_default) {
605                 /* remove custom just in case */
606                 remove_custom = TRUE;
607         } else if (! is_custom && ! is_default) {
608                 create_custom_theme (parent);
609                 save_alert_sounds (chooser, alert_id);
610                 add_custom = TRUE;
611         } else if (is_custom && is_default) {
612                 save_alert_sounds (chooser, alert_id);
613                 /* after removing files check if it is empty */
614                 if (custom_theme_dir_is_empty ()) {
615                         remove_custom = TRUE;
616                 }
617         } else if (is_custom && ! is_default) {
618                 save_alert_sounds (chooser, alert_id);
619         }
620 
621         if (add_custom) {
622                 gtk_list_store_insert_with_values (GTK_LIST_STORE (theme_model),
623                                                    NULL,
624                                                    G_MAXINT,
625                                                    THEME_DISPLAY_COL, _("Custom"),
626                                                    THEME_IDENTIFIER_COL, CUSTOM_THEME_NAME,
627                                                    THEME_PARENT_ID_COL, theme,
628                                                    -1);
629                 set_combox_for_theme_name (chooser, CUSTOM_THEME_NAME);
630         } else if (remove_custom) {
631                 gtk_tree_model_get_iter_first (theme_model, &iter);
632                 do {
633                         char *this_parent;
634 
635                         gtk_tree_model_get (theme_model, &iter,
636                                             THEME_PARENT_ID_COL, &this_parent,
637                                             -1);
638                         if (this_parent != NULL && strcmp (this_parent, CUSTOM_THEME_NAME) != 0) {
639                                 g_free (this_parent);
640                                 gtk_list_store_remove (GTK_LIST_STORE (theme_model), &iter);
641                                 break;
642                         }
643                         g_free (this_parent);
644                 } while (gtk_tree_model_iter_next (theme_model, &iter));
645 
646                 delete_custom_theme_dir ();
647 
648                 set_combox_for_theme_name (chooser, parent);
649         }
650 
651         update_alert_model (chooser, alert_id);
652 
653         g_free (theme);
654         g_free (parent);
655 }
656 
657 static void
on_alert_toggled(GtkCellRendererToggle * renderer,char * path_str,GvcSoundThemeChooser * chooser)658 on_alert_toggled (GtkCellRendererToggle *renderer,
659                   char                  *path_str,
660                   GvcSoundThemeChooser  *chooser)
661 {
662         GtkTreeModel *model;
663         GtkTreeIter   iter;
664         GtkTreePath  *path;
665         gboolean      toggled;
666         char         *id;
667 
668         model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser->priv->treeview));
669 
670         path = gtk_tree_path_new_from_string (path_str);
671         gtk_tree_model_get_iter (model, &iter, path);
672         gtk_tree_path_free (path);
673 
674         id = NULL;
675         gtk_tree_model_get (model, &iter,
676                             ALERT_IDENTIFIER_COL, &id,
677                             ALERT_ACTIVE_COL, &toggled,
678                             -1);
679 
680         toggled ^= 1;
681         if (toggled) {
682                 update_alert (chooser, id);
683         }
684 
685         g_free (id);
686 }
687 
688 static void
play_preview_for_path(GvcSoundThemeChooser * chooser,GtkTreePath * path)689 play_preview_for_path (GvcSoundThemeChooser *chooser, GtkTreePath *path)
690 {
691         GtkTreeModel *model;
692         GtkTreeIter   iter;
693         GtkTreeIter   theme_iter;
694         gchar        *id = NULL;
695         gchar        *parent_theme = NULL;
696 
697         model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser->priv->treeview));
698         if (gtk_tree_model_get_iter (model, &iter, path) == FALSE)
699                 return;
700 
701         gtk_tree_model_get (model, &iter,
702                             ALERT_IDENTIFIER_COL, &id,
703                             -1);
704         if (id == NULL)
705                 return;
706 
707         if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (chooser->priv->combo_box), &theme_iter)) {
708                 GtkTreeModel *theme_model;
709                 gchar        *theme_id = NULL;
710                 gchar        *parent_id = NULL;
711 
712                 theme_model = gtk_combo_box_get_model (GTK_COMBO_BOX (chooser->priv->combo_box));
713 
714                 gtk_tree_model_get (theme_model, &theme_iter,
715                                     THEME_IDENTIFIER_COL, &theme_id,
716                                     THEME_PARENT_ID_COL, &parent_id, -1);
717                 if (theme_id && strcmp (theme_id, CUSTOM_THEME_NAME) == 0)
718                         parent_theme = g_strdup (parent_id);
719 
720                 g_free (theme_id);
721                 g_free (parent_id);
722         }
723 
724         /* special case: for the default item on custom themes
725          * play the alert for the parent theme */
726         if (strcmp (id, DEFAULT_ALERT_ID) == 0) {
727                 if (parent_theme != NULL) {
728                         ca_gtk_play_for_widget (GTK_WIDGET (chooser), 0,
729                                                 CA_PROP_APPLICATION_NAME, _("Sound Preferences"),
730                                                 CA_PROP_EVENT_ID, "bell-window-system",
731                                                 CA_PROP_CANBERRA_XDG_THEME_NAME, parent_theme,
732                                                 CA_PROP_EVENT_DESCRIPTION, _("Testing event sound"),
733                                                 CA_PROP_CANBERRA_CACHE_CONTROL, "never",
734                                                 CA_PROP_APPLICATION_ID, "org.mate.VolumeControl",
735 #ifdef CA_PROP_CANBERRA_ENABLE
736                                                 CA_PROP_CANBERRA_ENABLE, "1",
737 #endif
738                                                 NULL);
739                 } else {
740                         ca_gtk_play_for_widget (GTK_WIDGET (chooser), 0,
741                                                 CA_PROP_APPLICATION_NAME, _("Sound Preferences"),
742                                                 CA_PROP_EVENT_ID, "bell-window-system",
743                                                 CA_PROP_EVENT_DESCRIPTION, _("Testing event sound"),
744                                                 CA_PROP_CANBERRA_CACHE_CONTROL, "never",
745                                                 CA_PROP_APPLICATION_ID, "org.mate.VolumeControl",
746 #ifdef CA_PROP_CANBERRA_ENABLE
747                                                 CA_PROP_CANBERRA_ENABLE, "1",
748 #endif
749                                                 NULL);
750                 }
751         } else {
752                 ca_gtk_play_for_widget (GTK_WIDGET (chooser), 0,
753                                         CA_PROP_APPLICATION_NAME, _("Sound Preferences"),
754                                         CA_PROP_MEDIA_FILENAME, id,
755                                         CA_PROP_EVENT_DESCRIPTION, _("Testing event sound"),
756                                         CA_PROP_CANBERRA_CACHE_CONTROL, "never",
757                                         CA_PROP_APPLICATION_ID, "org.mate.VolumeControl",
758 #ifdef CA_PROP_CANBERRA_ENABLE
759                                         CA_PROP_CANBERRA_ENABLE, "1",
760 #endif
761                                         NULL);
762 
763         }
764         g_free (parent_theme);
765         g_free (id);
766 }
767 
768 static void
on_treeview_row_activated(GtkTreeView * treeview,GtkTreePath * path,GtkTreeViewColumn * column,GvcSoundThemeChooser * chooser)769 on_treeview_row_activated (GtkTreeView          *treeview,
770                            GtkTreePath          *path,
771                            GtkTreeViewColumn    *column,
772                            GvcSoundThemeChooser *chooser)
773 {
774         play_preview_for_path (chooser, path);
775 }
776 
777 static void
on_treeview_selection_changed(GtkTreeSelection * selection,GvcSoundThemeChooser * chooser)778 on_treeview_selection_changed (GtkTreeSelection     *selection,
779                                GvcSoundThemeChooser *chooser)
780 {
781         GList        *paths;
782         GtkTreeModel *model;
783         GtkTreePath  *path;
784 
785         if (chooser->priv->treeview == NULL)
786                 return;
787 
788         model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser->priv->treeview));
789 
790         paths = gtk_tree_selection_get_selected_rows (selection, &model);
791         if (paths == NULL)
792                 return;
793 
794         path = paths->data;
795         play_preview_for_path (chooser, path);
796 
797         g_list_foreach (paths, (GFunc)gtk_tree_path_free, NULL);
798         g_list_free (paths);
799 }
800 
801 static GtkWidget *
create_alert_treeview(GvcSoundThemeChooser * chooser)802 create_alert_treeview (GvcSoundThemeChooser *chooser)
803 {
804         GtkListStore         *store;
805         GtkWidget            *treeview;
806         GtkCellRenderer      *renderer;
807         GtkTreeViewColumn    *column;
808         GtkTreeSelection     *selection;
809 
810         treeview = gtk_tree_view_new ();
811 
812         gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE);
813         g_signal_connect (G_OBJECT (treeview),
814                           "row-activated",
815                           G_CALLBACK (on_treeview_row_activated),
816                           chooser);
817 
818         selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
819 
820         gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);
821         g_signal_connect (G_OBJECT (selection),
822                           "changed",
823                           G_CALLBACK (on_treeview_selection_changed),
824                           chooser);
825 
826         /* Setup the tree model, 3 columns:
827          * - display name
828          * - sound id
829          * - sound type
830          */
831         store = gtk_list_store_new (ALERT_NUM_COLS,
832                                     G_TYPE_STRING,
833                                     G_TYPE_STRING,
834                                     G_TYPE_STRING,
835                                     G_TYPE_BOOLEAN);
836 
837         gtk_list_store_insert_with_values (store,
838                                            NULL,
839                                            G_MAXINT,
840                                            ALERT_IDENTIFIER_COL, DEFAULT_ALERT_ID,
841                                            ALERT_DISPLAY_COL, _("Default"),
842                                            ALERT_SOUND_TYPE_COL, _("From theme"),
843                                            ALERT_ACTIVE_COL, TRUE,
844                                            -1);
845 
846         populate_model_from_dir (chooser, GTK_TREE_MODEL (store), SOUND_SET_DIR);
847 
848         gtk_tree_view_set_model (GTK_TREE_VIEW (treeview),
849                                  GTK_TREE_MODEL (store));
850 
851         renderer = gtk_cell_renderer_toggle_new ();
852         gtk_cell_renderer_toggle_set_radio (GTK_CELL_RENDERER_TOGGLE (renderer), TRUE);
853 
854         column = gtk_tree_view_column_new_with_attributes (NULL,
855                                                            renderer,
856                                                            "active", ALERT_ACTIVE_COL,
857                                                            NULL);
858         gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
859         g_signal_connect (renderer,
860                           "toggled",
861                           G_CALLBACK (on_alert_toggled),
862                           chooser);
863 
864         renderer = gtk_cell_renderer_text_new ();
865         column = gtk_tree_view_column_new_with_attributes (_("Name"),
866                                                            renderer,
867                                                            "text", ALERT_DISPLAY_COL,
868                                                            NULL);
869         gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
870 
871         renderer = gtk_cell_renderer_text_new ();
872         column = gtk_tree_view_column_new_with_attributes (_("Type"),
873                                                            renderer,
874                                                            "text", ALERT_SOUND_TYPE_COL,
875                                                            NULL);
876 
877         gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
878 
879         return treeview;
880 }
881 
882 static int
get_file_type(const char * sound_name,char ** linked_name)883 get_file_type (const char *sound_name,
884                char      **linked_name)
885 {
886         char *name, *filename;
887 
888         *linked_name = NULL;
889 
890         name = g_strdup_printf ("%s.disabled", sound_name);
891         filename = custom_theme_dir_path (name);
892         g_free (name);
893 
894         if (g_file_test (filename, G_FILE_TEST_IS_REGULAR) != FALSE) {
895                 g_free (filename);
896                 return SOUND_TYPE_OFF;
897         }
898         g_free (filename);
899 
900         /* We only check for .ogg files because those are the
901          * only ones we create */
902         name = g_strdup_printf ("%s.ogg", sound_name);
903         filename = custom_theme_dir_path (name);
904         g_free (name);
905 
906         if (g_file_test (filename, G_FILE_TEST_IS_SYMLINK) != FALSE) {
907                 *linked_name = g_file_read_link (filename, NULL);
908                 g_free (filename);
909                 return SOUND_TYPE_CUSTOM;
910         }
911         g_free (filename);
912 
913         return SOUND_TYPE_BUILTIN;
914 }
915 
916 static void
update_alerts_from_theme_name(GvcSoundThemeChooser * chooser,const gchar * name)917 update_alerts_from_theme_name (GvcSoundThemeChooser *chooser,
918                                const gchar          *name)
919 {
920         if (strcmp (name, CUSTOM_THEME_NAME) != 0) {
921                 /* reset alert to default */
922                 update_alert (chooser, DEFAULT_ALERT_ID);
923         } else {
924                 int   sound_type;
925                 char *linkname;
926 
927                 linkname = NULL;
928                 sound_type = get_file_type ("bell-terminal", &linkname);
929                 g_debug ("Found link: %s", linkname);
930                 if (sound_type == SOUND_TYPE_CUSTOM) {
931                         update_alert (chooser, linkname);
932                 }
933         }
934 }
935 
936 static void
update_theme(GvcSoundThemeChooser * chooser)937 update_theme (GvcSoundThemeChooser *chooser)
938 {
939         char        *theme_name;
940         gboolean     events_enabled;
941         gboolean     feedback_enabled;
942 
943         feedback_enabled = g_settings_get_boolean (chooser->priv->sound_settings, INPUT_SOUNDS_KEY);
944         set_input_feedback_enabled (chooser, feedback_enabled);
945 
946         events_enabled = g_settings_get_boolean (chooser->priv->sound_settings, EVENT_SOUNDS_KEY);
947         if (events_enabled) {
948                 theme_name = g_settings_get_string (chooser->priv->sound_settings, SOUND_THEME_KEY);
949         } else {
950                 theme_name = g_strdup (NO_SOUNDS_THEME_NAME);
951         }
952 
953         gtk_widget_set_sensitive (chooser->priv->selection_box, events_enabled);
954         gtk_widget_set_sensitive (chooser->priv->click_feedback_button, events_enabled);
955 
956         set_combox_for_theme_name (chooser, theme_name);
957 
958         update_alerts_from_theme_name (chooser, theme_name);
959 
960         g_free (theme_name);
961 }
962 
963 static void
gvc_sound_theme_chooser_class_init(GvcSoundThemeChooserClass * klass)964 gvc_sound_theme_chooser_class_init (GvcSoundThemeChooserClass *klass)
965 {
966         GObjectClass *object_class = G_OBJECT_CLASS (klass);
967 
968         object_class->dispose = gvc_sound_theme_chooser_dispose;
969 }
970 
971 static void
on_click_feedback_toggled(GtkToggleButton * button,GvcSoundThemeChooser * chooser)972 on_click_feedback_toggled (GtkToggleButton      *button,
973                            GvcSoundThemeChooser *chooser)
974 {
975         gboolean enabled;
976 
977         enabled = gtk_toggle_button_get_active (button);
978 
979         g_settings_set_boolean (chooser->priv->sound_settings, INPUT_SOUNDS_KEY, enabled);
980 }
981 
982 static void
on_key_changed(GSettings * settings,gchar * key,GvcSoundThemeChooser * chooser)983 on_key_changed (GSettings            *settings,
984                 gchar                *key,
985                 GvcSoundThemeChooser *chooser)
986 {
987         if (!strcmp (key, EVENT_SOUNDS_KEY) ||
988             !strcmp (key, SOUND_THEME_KEY) ||
989             !strcmp (key, INPUT_SOUNDS_KEY))
990                 update_theme (chooser);
991 }
992 
993 static void
setup_list_size_constraint(GtkWidget * widget,GtkWidget * to_size)994 setup_list_size_constraint (GtkWidget *widget,
995                             GtkWidget *to_size)
996 {
997         GtkRequisition req;
998         gint           sc_height;
999         int            max_height;
1000 
1001         /* Constrain height to be the tree height up to a max */
1002         gdk_window_get_geometry (gdk_screen_get_root_window (gtk_widget_get_screen (widget)),
1003                                  NULL, NULL, NULL, &sc_height);
1004 
1005         max_height = sc_height / 4;
1006 
1007         // XXX this doesn't work
1008         gtk_widget_get_preferred_size (to_size, NULL, &req);
1009 
1010         gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (widget),
1011                                                     MIN (req.height, max_height));
1012 }
1013 
1014 static void
gvc_sound_theme_chooser_init(GvcSoundThemeChooser * chooser)1015 gvc_sound_theme_chooser_init (GvcSoundThemeChooser *chooser)
1016 {
1017         GtkWidget   *box;
1018         GtkWidget   *label;
1019         GtkWidget   *scrolled_window;
1020         gchar       *str;
1021 
1022         chooser->priv = gvc_sound_theme_chooser_get_instance_private (chooser);
1023 
1024         chooser->priv->theme_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
1025 
1026         gtk_box_pack_start (GTK_BOX (chooser),
1027                             chooser->priv->theme_box, FALSE, FALSE, 0);
1028 
1029         label = gtk_label_new_with_mnemonic (_("Sound _theme:"));
1030         gtk_box_pack_start (GTK_BOX (chooser->priv->theme_box), label, FALSE, FALSE, 0);
1031         chooser->priv->combo_box = gtk_combo_box_new ();
1032         gtk_box_pack_start (GTK_BOX (chooser->priv->theme_box), chooser->priv->combo_box, FALSE, FALSE, 6);
1033         gtk_label_set_mnemonic_widget (GTK_LABEL (label), chooser->priv->combo_box);
1034 
1035         chooser->priv->sound_settings = g_settings_new (KEY_SOUNDS_SCHEMA);
1036 
1037         g_signal_connect (G_OBJECT (chooser->priv->sound_settings),
1038                           "changed",
1039                           G_CALLBACK (on_key_changed),
1040                           chooser);
1041 
1042         str = g_strdup_printf ("<b>%s</b>", _("C_hoose an alert sound:"));
1043         chooser->priv->selection_box = box = gtk_frame_new (str);
1044         g_free (str);
1045 
1046         label = gtk_frame_get_label_widget (GTK_FRAME (box));
1047         gtk_label_set_use_underline (GTK_LABEL (label), TRUE);
1048         gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
1049         gtk_frame_set_shadow_type (GTK_FRAME (box), GTK_SHADOW_NONE);
1050 
1051         gtk_box_pack_start (GTK_BOX (chooser), box, TRUE, TRUE, 6);
1052 
1053         chooser->priv->treeview = create_alert_treeview (chooser);
1054         gtk_label_set_mnemonic_widget (GTK_LABEL (label), chooser->priv->treeview);
1055 
1056         scrolled_window = gtk_scrolled_window_new (NULL, NULL);
1057         gtk_widget_set_hexpand (scrolled_window, TRUE);
1058         gtk_widget_set_vexpand (scrolled_window, TRUE);
1059         gtk_widget_set_margin_top (scrolled_window, 6);
1060 
1061         gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
1062                                         GTK_POLICY_NEVER,
1063                                         GTK_POLICY_AUTOMATIC);
1064         gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window),
1065                                              GTK_SHADOW_IN);
1066 
1067         gtk_container_add (GTK_CONTAINER (scrolled_window), chooser->priv->treeview);
1068         gtk_container_add (GTK_CONTAINER (box), scrolled_window);
1069 
1070         chooser->priv->click_feedback_button = gtk_check_button_new_with_mnemonic (_("Enable _window and button sounds"));
1071         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (chooser->priv->click_feedback_button),
1072                                       g_settings_get_boolean (chooser->priv->sound_settings, INPUT_SOUNDS_KEY));
1073 
1074         gtk_box_pack_start (GTK_BOX (chooser),
1075                             chooser->priv->click_feedback_button,
1076                             FALSE, FALSE, 0);
1077 
1078         g_signal_connect (G_OBJECT (chooser->priv->click_feedback_button),
1079                           "toggled",
1080                           G_CALLBACK (on_click_feedback_toggled),
1081                           chooser);
1082 
1083         setup_theme_selector (chooser);
1084         update_theme (chooser);
1085 
1086         setup_list_size_constraint (scrolled_window, chooser->priv->treeview);
1087 }
1088 
1089 static void
gvc_sound_theme_chooser_dispose(GObject * object)1090 gvc_sound_theme_chooser_dispose (GObject *object)
1091 {
1092         GvcSoundThemeChooser *chooser;
1093 
1094         chooser = GVC_SOUND_THEME_CHOOSER (object);
1095 
1096         g_clear_object (&chooser->priv->sound_settings);
1097 
1098         G_OBJECT_CLASS (gvc_sound_theme_chooser_parent_class)->dispose (object);
1099 }
1100 
1101 GtkWidget *
gvc_sound_theme_chooser_new(void)1102 gvc_sound_theme_chooser_new (void)
1103 {
1104         return g_object_new (GVC_TYPE_SOUND_THEME_CHOOSER,
1105                                 "spacing", 6,
1106                                 "orientation", GTK_ORIENTATION_VERTICAL,
1107                                 NULL);
1108 }
1109