/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2008 Bastien Nocera * Copyright (C) 2008 William Jon McCann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include "gvc-sound-theme-chooser.h" #include "sound-theme-file-utils.h" #define GVC_SOUND_THEME_CHOOSER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_SOUND_THEME_CHOOSER, GvcSoundThemeChooserPrivate)) struct GvcSoundThemeChooserPrivate { GtkWidget *treeview; GtkWidget *selection_box; GSettings *settings; GSettings *sound_settings; char *current_theme; char *current_parent; }; static void gvc_sound_theme_chooser_class_init (GvcSoundThemeChooserClass *klass); static void gvc_sound_theme_chooser_init (GvcSoundThemeChooser *sound_theme_chooser); static void gvc_sound_theme_chooser_finalize (GObject *object); G_DEFINE_TYPE (GvcSoundThemeChooser, gvc_sound_theme_chooser, GTK_TYPE_BOX) #define KEY_SOUNDS_SCHEMA "org.gnome.desktop.sound" #define EVENT_SOUNDS_KEY "event-sounds" #define INPUT_SOUNDS_KEY "input-feedback-sounds" #define SOUND_THEME_KEY "theme-name" #define WM_SCHEMA "org.gnome.desktop.wm.preferences" #define AUDIO_BELL_KEY "audible-bell" #define DEFAULT_ALERT_ID "__default" #define CUSTOM_THEME_NAME "__custom" #define NO_SOUNDS_THEME_NAME "__no_sounds" #define DEFAULT_THEME "ubuntu" enum { THEME_DISPLAY_COL, THEME_IDENTIFIER_COL, THEME_PARENT_ID_COL, THEME_NUM_COLS }; enum { ALERT_DISPLAY_COL, ALERT_IDENTIFIER_COL, ALERT_SOUND_TYPE_COL, ALERT_NUM_COLS }; enum { SOUND_TYPE_UNSET, SOUND_TYPE_OFF, SOUND_TYPE_DEFAULT_FROM_THEME, SOUND_TYPE_BUILTIN, SOUND_TYPE_CUSTOM }; #define GVC_SOUND_SOUND (xmlChar *) "sound" #define GVC_SOUND_NAME (xmlChar *) "name" #define GVC_SOUND_FILENAME (xmlChar *) "filename" /* Adapted from yelp-toc-pager.c */ static xmlChar * xml_get_and_trim_names (xmlNodePtr node) { xmlNodePtr cur; xmlChar *keep_lang = NULL; xmlChar *value; int j, keep_pri = INT_MAX; const gchar * const * langs = g_get_language_names (); value = NULL; for (cur = node->children; cur; cur = cur->next) { if (! xmlStrcmp (cur->name, GVC_SOUND_NAME)) { xmlChar *cur_lang = NULL; int cur_pri = INT_MAX; cur_lang = xmlNodeGetLang (cur); if (cur_lang) { for (j = 0; langs[j]; j++) { if (g_str_equal (cur_lang, langs[j])) { cur_pri = j; break; } } } else { cur_pri = INT_MAX - 1; } if (cur_pri <= keep_pri) { if (keep_lang) xmlFree (keep_lang); if (value) xmlFree (value); value = xmlNodeGetContent (cur); keep_lang = cur_lang; keep_pri = cur_pri; } else { if (cur_lang) xmlFree (cur_lang); } } } /* Delete all GVC_SOUND_NAME nodes */ cur = node->children; while (cur) { xmlNodePtr this = cur; cur = cur->next; if (! xmlStrcmp (this->name, GVC_SOUND_NAME)) { xmlUnlinkNode (this); xmlFreeNode (this); } } return value; } static void populate_model_from_node (GvcSoundThemeChooser *chooser, GtkTreeModel *model, xmlNodePtr node) { xmlNodePtr child; xmlChar *filename; xmlChar *name; filename = NULL; name = xml_get_and_trim_names (node); for (child = node->children; child; child = child->next) { if (xmlNodeIsText (child)) { continue; } if (xmlStrcmp (child->name, GVC_SOUND_FILENAME) == 0) { filename = xmlNodeGetContent (child); } else if (xmlStrcmp (child->name, GVC_SOUND_NAME) == 0) { /* EH? should have been trimmed */ } } if (filename != NULL && name != NULL) { gtk_list_store_insert_with_values (GTK_LIST_STORE (model), NULL, G_MAXINT, ALERT_IDENTIFIER_COL, filename, ALERT_DISPLAY_COL, name, ALERT_SOUND_TYPE_COL, _("Built-in"), -1); } xmlFree (filename); xmlFree (name); } static void populate_model_from_file (GvcSoundThemeChooser *chooser, GtkTreeModel *model, const char *filename) { xmlDocPtr doc; xmlNodePtr root; xmlNodePtr child; gboolean exists; exists = g_file_test (filename, G_FILE_TEST_EXISTS); if (! exists) { return; } doc = xmlParseFile (filename); if (doc == NULL) { return; } root = xmlDocGetRootElement (doc); for (child = root->children; child; child = child->next) { if (xmlNodeIsText (child)) { continue; } if (xmlStrcmp (child->name, GVC_SOUND_SOUND) != 0) { continue; } populate_model_from_node (chooser, model, child); } xmlFreeDoc (doc); } static void populate_model_from_dir (GvcSoundThemeChooser *chooser, GtkTreeModel *model, const char *dirname) { GDir *d; const char *name; d = g_dir_open (dirname, 0, NULL); if (d == NULL) { return; } while ((name = g_dir_read_name (d)) != NULL) { char *path; if (! g_str_has_suffix (name, ".xml")) { continue; } path = g_build_filename (dirname, name, NULL); populate_model_from_file (chooser, model, path); g_free (path); } } static gboolean save_alert_sounds (GvcSoundThemeChooser *chooser, const char *id) { const char *sounds[3] = { "bell-terminal", "bell-window-system", NULL }; char *path; if (strcmp (id, DEFAULT_ALERT_ID) == 0) { delete_old_files (sounds); delete_disabled_files (sounds); } else { delete_old_files (sounds); delete_disabled_files (sounds); add_custom_file (sounds, id); } /* And poke the directory so the theme gets updated */ path = custom_theme_dir_path (NULL); if (utime (path, NULL) != 0) { g_warning ("Failed to update mtime for directory '%s': %s", path, g_strerror (errno)); } g_free (path); return FALSE; } static void update_alert_model (GvcSoundThemeChooser *chooser, const char *id) { GtkTreeModel *model; GtkTreeIter iter; model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser->priv->treeview)); gtk_tree_model_get_iter_first (model, &iter); do { char *this_id; gtk_tree_model_get (model, &iter, ALERT_IDENTIFIER_COL, &this_id, -1); if (strcmp (this_id, id) == 0) { GtkTreeSelection *selection; selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (chooser->priv->treeview)); gtk_tree_selection_select_iter (selection, &iter); } g_free (this_id); } while (gtk_tree_model_iter_next (model, &iter)); } static void save_theme_name (GvcSoundThemeChooser *chooser, const char *theme_name) { /* If the name is empty, use "freedesktop" */ if (theme_name == NULL || *theme_name == '\0') { theme_name = DEFAULT_THEME; } /* special case for no sounds */ if (strcmp (theme_name, NO_SOUNDS_THEME_NAME) == 0) { g_settings_set_boolean (chooser->priv->sound_settings, EVENT_SOUNDS_KEY, FALSE); return; } else { g_settings_set_boolean (chooser->priv->sound_settings, EVENT_SOUNDS_KEY, TRUE); } g_settings_set_string (chooser->priv->sound_settings, SOUND_THEME_KEY, theme_name); } static gboolean load_theme_file (const char *path, char **parent) { GKeyFile *file; gboolean hidden; file = g_key_file_new (); if (g_key_file_load_from_file (file, path, G_KEY_FILE_KEEP_TRANSLATIONS, NULL) == FALSE) { g_key_file_free (file); return FALSE; } /* Don't add hidden themes to the list */ hidden = g_key_file_get_boolean (file, "Sound Theme", "Hidden", NULL); if (!hidden) { /* Save the parent theme, if there's one */ if (parent != NULL) { *parent = g_key_file_get_string (file, "Sound Theme", "Inherits", NULL); } } g_key_file_free (file); return TRUE; } static gboolean load_theme_name (const char *name, char **parent) { const char * const *data_dirs; const char *data_dir; char *path; guint i; gboolean res; data_dir = g_get_user_data_dir (); path = g_build_filename (data_dir, "sounds", name, "index.theme", NULL); res = load_theme_file (path, parent); g_free (path); if (res) return TRUE; data_dirs = g_get_system_data_dirs (); for (i = 0; data_dirs[i] != NULL; i++) { path = g_build_filename (data_dirs[i], "sounds", name, "index.theme", NULL); res = load_theme_file (path, parent); g_free (path); if (res) return TRUE; } return FALSE; } static void update_alert (GvcSoundThemeChooser *chooser, const char *alert_id) { gboolean is_custom; gboolean is_default; gboolean add_custom; gboolean remove_custom; is_custom = strcmp (chooser->priv->current_theme, CUSTOM_THEME_NAME) == 0; is_default = strcmp (alert_id, DEFAULT_ALERT_ID) == 0; /* So a few possibilities: * 1. Named theme, default alert selected: noop * 2. Named theme, alternate alert selected: create new custom with sound * 3. Custom theme, default alert selected: remove sound and possibly custom * 4. Custom theme, alternate alert selected: update custom sound */ add_custom = FALSE; remove_custom = FALSE; if (! is_custom && is_default) { /* remove custom just in case */ remove_custom = TRUE; } else if (! is_custom && ! is_default) { if (chooser->priv->current_parent) create_custom_theme (chooser->priv->current_parent); else create_custom_theme (DEFAULT_THEME); save_alert_sounds (chooser, alert_id); add_custom = TRUE; } else if (is_custom && is_default) { save_alert_sounds (chooser, alert_id); /* after removing files check if it is empty */ if (custom_theme_dir_is_empty ()) { remove_custom = TRUE; } } else if (is_custom && ! is_default) { save_alert_sounds (chooser, alert_id); } if (add_custom) { save_theme_name (chooser, CUSTOM_THEME_NAME); } else if (remove_custom) { delete_custom_theme_dir (); if (is_custom) { save_theme_name (chooser, chooser->priv->current_parent); } } update_alert_model (chooser, alert_id); } static void play_preview_for_id (GvcSoundThemeChooser *chooser, const char *id) { g_return_if_fail (id != NULL); /* special case: for the default item on custom themes * play the alert for the parent theme */ if (strcmp (id, DEFAULT_ALERT_ID) == 0) { if (chooser->priv->current_parent != NULL) { ca_gtk_play_for_widget (GTK_WIDGET (chooser), 0, CA_PROP_APPLICATION_NAME, _("Sound Preferences"), CA_PROP_EVENT_ID, "bell-window-system", CA_PROP_CANBERRA_XDG_THEME_NAME, chooser->priv->current_parent, CA_PROP_EVENT_DESCRIPTION, _("Testing event sound"), CA_PROP_CANBERRA_CACHE_CONTROL, "never", CA_PROP_APPLICATION_ID, "org.gnome.VolumeControl", #ifdef CA_PROP_CANBERRA_ENABLE CA_PROP_CANBERRA_ENABLE, "1", #endif NULL); } else { ca_gtk_play_for_widget (GTK_WIDGET (chooser), 0, CA_PROP_APPLICATION_NAME, _("Sound Preferences"), CA_PROP_EVENT_ID, "bell-window-system", CA_PROP_EVENT_DESCRIPTION, _("Testing event sound"), CA_PROP_CANBERRA_CACHE_CONTROL, "never", CA_PROP_APPLICATION_ID, "org.gnome.VolumeControl", #ifdef CA_PROP_CANBERRA_ENABLE CA_PROP_CANBERRA_ENABLE, "1", #endif NULL); } } else { ca_gtk_play_for_widget (GTK_WIDGET (chooser), 0, CA_PROP_APPLICATION_NAME, _("Sound Preferences"), CA_PROP_MEDIA_FILENAME, id, CA_PROP_EVENT_DESCRIPTION, _("Testing event sound"), CA_PROP_CANBERRA_CACHE_CONTROL, "never", CA_PROP_APPLICATION_ID, "org.gnome.VolumeControl", #ifdef CA_PROP_CANBERRA_ENABLE CA_PROP_CANBERRA_ENABLE, "1", #endif NULL); } } static void on_treeview_selection_changed (GtkTreeSelection *selection, GvcSoundThemeChooser *chooser) { GtkTreeModel *model; GtkTreeIter iter; char *id; if (chooser->priv->treeview == NULL) { return; } model = gtk_tree_view_get_model (GTK_TREE_VIEW (chooser->priv->treeview)); if (gtk_tree_selection_get_selected (selection, &model, &iter) == FALSE) { return; } id = NULL; gtk_tree_model_get (model, &iter, ALERT_IDENTIFIER_COL, &id, -1); if (id == NULL) { return; } play_preview_for_id (chooser, id); update_alert (chooser, id); g_free (id); } static gboolean on_treeview_button_pressed (GtkTreeView *treeview, GdkEventButton *event, GvcSoundThemeChooser *chooser) { GtkTreeSelection *selection; GtkTreePath *path; selection = gtk_tree_view_get_selection (treeview); if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview), event->x, event->y, &path, NULL, NULL, NULL) == FALSE) { return FALSE; } if (gtk_tree_selection_path_is_selected (selection, path) == FALSE) { gtk_tree_path_free (path); return FALSE; } gtk_tree_path_free (path); on_treeview_selection_changed (selection, chooser); return FALSE; } static GtkWidget * create_alert_treeview (GvcSoundThemeChooser *chooser) { GtkListStore *store; GtkWidget *treeview; GtkCellRenderer *renderer; GtkTreeViewColumn *column; GtkTreeSelection *selection; treeview = gtk_tree_view_new (); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview), FALSE); g_signal_connect (treeview, "button-press-event", G_CALLBACK (on_treeview_button_pressed), chooser); selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview)); gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE); g_signal_connect (selection, "changed", G_CALLBACK (on_treeview_selection_changed), chooser); /* Setup the tree model, 3 columns: * - display name * - sound id * - sound type */ store = gtk_list_store_new (ALERT_NUM_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); gtk_list_store_insert_with_values (store, NULL, G_MAXINT, ALERT_IDENTIFIER_COL, DEFAULT_ALERT_ID, ALERT_DISPLAY_COL, _("Default"), ALERT_SOUND_TYPE_COL, _("From theme"), -1); populate_model_from_dir (chooser, GTK_TREE_MODEL (store), SOUND_SET_DIR); gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (store)); renderer = gtk_cell_renderer_text_new (); column = gtk_tree_view_column_new_with_attributes (_("Name"), renderer, "text", ALERT_DISPLAY_COL, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column); return treeview; } static int get_file_type (const char *sound_name, char **linked_name) { char *name, *filename; *linked_name = NULL; name = g_strdup_printf ("%s.disabled", sound_name); filename = custom_theme_dir_path (name); g_free (name); if (g_file_test (filename, G_FILE_TEST_IS_REGULAR) != FALSE) { g_free (filename); return SOUND_TYPE_OFF; } g_free (filename); /* We only check for .ogg files because those are the * only ones we create */ name = g_strdup_printf ("%s.ogg", sound_name); filename = custom_theme_dir_path (name); g_free (name); if (g_file_test (filename, G_FILE_TEST_IS_SYMLINK) != FALSE) { *linked_name = g_file_read_link (filename, NULL); g_free (filename); return SOUND_TYPE_CUSTOM; } g_free (filename); return SOUND_TYPE_BUILTIN; } static void update_alerts_from_theme_name (GvcSoundThemeChooser *chooser, const char *name) { if (strcmp (name, CUSTOM_THEME_NAME) != 0) { /* reset alert to default */ update_alert (chooser, DEFAULT_ALERT_ID); } else { int sound_type; char *linkname; linkname = NULL; sound_type = get_file_type ("bell-terminal", &linkname); g_debug ("Found link: %s", linkname); if (sound_type == SOUND_TYPE_CUSTOM) { update_alert (chooser, linkname); } } } static void update_theme (GvcSoundThemeChooser *chooser) { gboolean events_enabled; char *last_theme; events_enabled = g_settings_get_boolean (chooser->priv->sound_settings, EVENT_SOUNDS_KEY); last_theme = chooser->priv->current_theme; if (events_enabled) { chooser->priv->current_theme = g_settings_get_string (chooser->priv->sound_settings, SOUND_THEME_KEY); } else { chooser->priv->current_theme = g_strdup (NO_SOUNDS_THEME_NAME); } if (g_strcmp0 (last_theme, chooser->priv->current_theme) != 0) { g_free (chooser->priv->current_parent); if (load_theme_name (chooser->priv->current_theme, &chooser->priv->current_parent) == FALSE) { g_free (chooser->priv->current_theme); chooser->priv->current_theme = g_strdup (DEFAULT_THEME); load_theme_name (DEFAULT_THEME, &chooser->priv->current_parent); } } g_free (last_theme); gtk_widget_set_sensitive (chooser->priv->selection_box, events_enabled); update_alerts_from_theme_name (chooser, chooser->priv->current_theme); } static GObject * gvc_sound_theme_chooser_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_params) { GObject *object; GvcSoundThemeChooser *self; object = G_OBJECT_CLASS (gvc_sound_theme_chooser_parent_class)->constructor (type, n_construct_properties, construct_params); self = GVC_SOUND_THEME_CHOOSER (object); update_theme (self); return object; } static void gvc_sound_theme_chooser_class_init (GvcSoundThemeChooserClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructor = gvc_sound_theme_chooser_constructor; object_class->finalize = gvc_sound_theme_chooser_finalize; g_type_class_add_private (klass, sizeof (GvcSoundThemeChooserPrivate)); } static void on_sound_settings_changed (GSettings *settings, const char *key, GvcSoundThemeChooser *chooser) { if (strcmp (key, EVENT_SOUNDS_KEY) == 0) { update_theme (chooser); } else if (strcmp (key, SOUND_THEME_KEY) == 0) { update_theme (chooser); } else if (strcmp (key, INPUT_SOUNDS_KEY) == 0) { update_theme (chooser); } } static void on_audible_bell_changed (GSettings *settings, const char *key, GvcSoundThemeChooser *chooser) { update_theme (chooser); } static void setup_list_size_constraint (GtkWidget *widget, GtkWidget *to_size) { GtkRequisition req; int max_height; /* constrain height to be the tree height up to a max */ max_height = (gdk_screen_get_height (gtk_widget_get_screen (widget))) / 4; gtk_widget_get_preferred_size (to_size, NULL, &req); gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (widget), MIN (req.height, max_height)); } static void gvc_sound_theme_chooser_init (GvcSoundThemeChooser *chooser) { GtkWidget *box; GtkWidget *label; GtkWidget *scrolled_window; GtkWidget *alignment; char *str; chooser->priv = GVC_SOUND_THEME_CHOOSER_GET_PRIVATE (chooser); chooser->priv->settings = g_settings_new (WM_SCHEMA); chooser->priv->sound_settings = g_settings_new (KEY_SOUNDS_SCHEMA); str = g_strdup_printf ("%s", _("C_hoose an alert sound:")); chooser->priv->selection_box = box = gtk_frame_new (str); g_free (str); label = gtk_frame_get_label_widget (GTK_FRAME (box)); gtk_label_set_use_underline (GTK_LABEL (label), TRUE); gtk_label_set_use_markup (GTK_LABEL (label), TRUE); gtk_frame_set_shadow_type (GTK_FRAME (box), GTK_SHADOW_NONE); alignment = gtk_alignment_new (0, 0, 1, 1); gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 0, 0, 0); gtk_container_add (GTK_CONTAINER (alignment), box); gtk_box_pack_start (GTK_BOX (chooser), alignment, TRUE, TRUE, 6); alignment = gtk_alignment_new (0, 0, 1, 1); gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 6, 0, 0, 0); gtk_container_add (GTK_CONTAINER (box), alignment); chooser->priv->treeview = create_alert_treeview (chooser); gtk_label_set_mnemonic_widget (GTK_LABEL (label), chooser->priv->treeview); scrolled_window = gtk_scrolled_window_new (NULL, NULL); setup_list_size_constraint (scrolled_window, chooser->priv->treeview); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN); gtk_container_add (GTK_CONTAINER (scrolled_window), chooser->priv->treeview); gtk_container_add (GTK_CONTAINER (alignment), scrolled_window); g_signal_connect (G_OBJECT (chooser->priv->sound_settings), "changed", G_CALLBACK (on_sound_settings_changed), chooser); g_signal_connect (chooser->priv->settings, "changed::" AUDIO_BELL_KEY, G_CALLBACK (on_audible_bell_changed), chooser); } static void gvc_sound_theme_chooser_finalize (GObject *object) { GvcSoundThemeChooser *sound_theme_chooser; g_return_if_fail (object != NULL); g_return_if_fail (GVC_IS_SOUND_THEME_CHOOSER (object)); sound_theme_chooser = GVC_SOUND_THEME_CHOOSER (object); if (sound_theme_chooser->priv != NULL) { g_object_unref (sound_theme_chooser->priv->settings); g_object_unref (sound_theme_chooser->priv->sound_settings); } G_OBJECT_CLASS (gvc_sound_theme_chooser_parent_class)->finalize (object); } GtkWidget * gvc_sound_theme_chooser_new (void) { GObject *chooser; chooser = g_object_new (GVC_TYPE_SOUND_THEME_CHOOSER, "spacing", 6, "orientation", GTK_ORIENTATION_VERTICAL, NULL); return GTK_WIDGET (chooser); }