/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2011, 2014 Red Hat, Inc. * * 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, see . * */ #include "config.h" #include #include #include #include #include #include "cc-keyboard-item.h" #define CUSTOM_KEYS_SCHEMA "org.gnome.settings-daemon.plugins.media-keys.custom-keybinding" struct _CcKeyboardItem { GObject parent_instance; CcKeyboardItem *reverse_item; gboolean is_reversed; gboolean hidden; CcKeyboardItemType type; BindingGroupType group; char *description; gboolean editable; GList *key_combos; GList *default_combos; gboolean can_set_multiple; /* GSettings path */ char *gsettings_path; gboolean desc_editable; char *command; gboolean cmd_editable; /* GSettings */ char *schema; char *key; GSettings *settings; }; enum { PROP_0, PROP_DESCRIPTION, PROP_EDITABLE, PROP_TYPE, PROP_IS_VALUE_DEFAULT, PROP_COMMAND, PROP_KEY_COMBOS }; static void cc_keyboard_item_class_init (CcKeyboardItemClass *klass); static void cc_keyboard_item_init (CcKeyboardItem *keyboard_item); static void cc_keyboard_item_finalize (GObject *object); G_DEFINE_TYPE (CcKeyboardItem, cc_keyboard_item, G_TYPE_OBJECT) static const CcKeyCombo EMPTY_COMBO = { 0, 0, 0 }; static gboolean combo_equal (CcKeyCombo *a, CcKeyCombo *b) { return (a->keyval == b->keyval && a->keycode == b->keycode && a->mask == b->mask); } static gboolean combos_contains (GList *combos, CcKeyCombo *needle) { for (GList *l = combos; l != NULL; l = l->next) { if (combo_equal (l->data, needle)) return TRUE; } return FALSE; } static gboolean combos_equal (GList *a, GList *b) { // Should be efficient enough for any sane number of bindings for (GList *l = a; l != NULL; l = l->next) { if (!combos_contains (b, l->data)) return FALSE; } for (GList *l = b; l != NULL; l = l->next) { if (!combos_contains (a, l->data)) return FALSE; } return TRUE; } static gboolean binding_from_string (const char *str, CcKeyCombo *combo) { g_return_val_if_fail (combo != NULL, FALSE); g_autofree guint *keycodes = NULL; if (str == NULL || strcmp (str, "disabled") == 0) { memset (combo, 0, sizeof(CcKeyCombo)); return TRUE; } gtk_accelerator_parse_with_keycode (str, gdk_display_get_default (), &combo->keyval, &keycodes, &combo->mask); combo->keycode = (keycodes ? keycodes[0] : 0); if (combo->keyval == 0) return FALSE; else return TRUE; } static void _set_description (CcKeyboardItem *item, const char *value) { g_free (item->description); item->description = g_strdup (value); } const char * cc_keyboard_item_get_description (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->description; } gboolean cc_keyboard_item_get_desc_editable (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE); return item->desc_editable; } static void _set_type (CcKeyboardItem *item, gint value) { item->type = value; } static void _set_command (CcKeyboardItem *item, const char *value) { g_free (item->command); item->command = g_strdup (value); } const char * cc_keyboard_item_get_command (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->command; } gboolean cc_keyboard_item_get_cmd_editable (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE); return item->cmd_editable; } static void cc_keyboard_item_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { CcKeyboardItem *self; self = CC_KEYBOARD_ITEM (object); switch (prop_id) { case PROP_DESCRIPTION: _set_description (self, g_value_get_string (value)); break; case PROP_COMMAND: _set_command (self, g_value_get_string (value)); break; case PROP_TYPE: _set_type (self, g_value_get_int (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void cc_keyboard_item_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { CcKeyboardItem *self; self = CC_KEYBOARD_ITEM (object); switch (prop_id) { case PROP_DESCRIPTION: g_value_set_string (value, self->description); break; case PROP_EDITABLE: g_value_set_boolean (value, self->editable); break; case PROP_COMMAND: g_value_set_string (value, self->command); break; case PROP_IS_VALUE_DEFAULT: g_value_set_boolean (value, cc_keyboard_item_is_value_default (self)); break; case PROP_KEY_COMBOS: g_value_set_pointer (value, self->key_combos); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void cc_keyboard_item_class_init (CcKeyboardItemClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = cc_keyboard_item_get_property; object_class->set_property = cc_keyboard_item_set_property; object_class->finalize = cc_keyboard_item_finalize; g_object_class_install_property (object_class, PROP_DESCRIPTION, g_param_spec_string ("description", "description", "description", NULL, G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_EDITABLE, g_param_spec_boolean ("editable", NULL, NULL, FALSE, G_PARAM_READABLE)); g_object_class_install_property (object_class, PROP_TYPE, g_param_spec_int ("type", NULL, NULL, CC_KEYBOARD_ITEM_TYPE_NONE, CC_KEYBOARD_ITEM_TYPE_GSETTINGS, CC_KEYBOARD_ITEM_TYPE_NONE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); g_object_class_install_property (object_class, PROP_COMMAND, g_param_spec_string ("command", "command", "command", NULL, G_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_IS_VALUE_DEFAULT, g_param_spec_boolean ("is-value-default", "is value default", "is value default", TRUE, G_PARAM_READABLE)); g_object_class_install_property (object_class, PROP_KEY_COMBOS, g_param_spec_pointer ("key-combos", "key combos", "key combos", G_PARAM_READABLE)); } static void cc_keyboard_item_init (CcKeyboardItem *item) { } static void cc_keyboard_item_finalize (GObject *object) { CcKeyboardItem *item; g_return_if_fail (object != NULL); g_return_if_fail (CC_IS_KEYBOARD_ITEM (object)); item = CC_KEYBOARD_ITEM (object); if (item->settings != NULL) g_object_unref (item->settings); /* Free memory */ g_free (item->gsettings_path); g_free (item->description); g_free (item->command); g_free (item->schema); g_free (item->key); g_list_free_full (item->key_combos, g_free); g_list_free_full (item->default_combos, g_free); G_OBJECT_CLASS (cc_keyboard_item_parent_class)->finalize (object); } CcKeyboardItem * cc_keyboard_item_new (CcKeyboardItemType type) { GObject *object; object = g_object_new (CC_TYPE_KEYBOARD_ITEM, "type", type, NULL); return CC_KEYBOARD_ITEM (object); } static guint * get_above_tab_keysyms (void) { guint keycode = 0x29 /* KEY_GRAVE */ + 8; g_autofree guint *keyvals = NULL; GArray *keysyms; int n_entries, i, j; keysyms = g_array_new (TRUE, FALSE, sizeof (guint)); if (!gdk_display_map_keycode (gdk_display_get_default (), keycode, NULL, &keyvals, &n_entries)) goto out; for (i = 0; i < n_entries; i++) { gboolean found = FALSE; for (j = 0; j < keysyms->len; j++) if (g_array_index (keysyms, guint, j) == keyvals[i]) { found = TRUE; break; } if (!found) g_array_append_val (keysyms, keyvals[i]); } out: return (guint *)g_array_free (keysyms, FALSE); } /* * translate_above_tab: * * @original_bindings: A list of accelerator strings * @new_bindings: (out): Translated bindings if translation is needed * * Translate accelerator strings that contain the Above_Tab fake keysym * used by mutter to strings that use the real keysyms that correspond * to the key that is located physically above the tab key. * * Returns: %TRUE if strings were translated, %FALSE if @original_bindings * can be used unmodified */ static gboolean translate_above_tab (char **original_bindings, char ***new_bindings) { GPtrArray *replaced_bindings; g_autofree guint *above_tab_keysyms = NULL; gboolean needs_translation = FALSE; char **str; for (str = original_bindings; *str && !needs_translation; str++) needs_translation = strstr (*str, "Above_Tab") != NULL; if (!needs_translation) return FALSE; above_tab_keysyms = get_above_tab_keysyms (); replaced_bindings = g_ptr_array_new (); for (str = original_bindings; *str; str++) { if (strstr (*str, "Above_Tab") == NULL) { g_ptr_array_add (replaced_bindings, g_strdup (*str)); } else { g_auto (GStrv) split_str = g_strsplit (*str, "Above_Tab", -1); int i; for (i = 0; above_tab_keysyms[i]; i++) { g_autofree char *sym = NULL; sym = gtk_accelerator_name (above_tab_keysyms[i], 0); g_ptr_array_add (replaced_bindings, g_strjoinv (sym, split_str)); } } g_ptr_array_add (replaced_bindings, NULL); } *new_bindings = (char **)g_ptr_array_free (replaced_bindings, FALSE); return TRUE; } static GList * variant_get_key_combos (GVariant *variant) { GList *combos = NULL; char **translated_bindings, **str; g_auto(GStrv) bindings = NULL; if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING)) { bindings = g_malloc0_n (2, sizeof(char *)); if (g_strcmp0 (g_variant_get_string (variant, NULL), "") != 0) bindings[0] = g_variant_dup_string (variant, NULL); } else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY)) { bindings = g_variant_dup_strv (variant, NULL); } if (translate_above_tab (bindings, &translated_bindings)) { g_strfreev (bindings); bindings = translated_bindings; } for (str = bindings; *str; str++) { g_autofree CcKeyCombo *combo = g_new (CcKeyCombo, 1); binding_from_string (*str, combo); if (combo->keyval != 0 || combo->keycode != 0 || combo->mask != 0) combos = g_list_prepend (combos, g_steal_pointer (&combo)); } return g_list_reverse (combos); } static GList * settings_get_key_combos (GSettings *settings, const char *key, gboolean use_default) { GList *key_combos; g_autoptr(GVariant) variant = NULL; if (use_default) variant = g_settings_get_default_value (settings, key); else variant = g_settings_get_value (settings, key); key_combos = variant_get_key_combos (variant); return key_combos; } static void binding_changed (CcKeyboardItem *item, const char *key) { g_list_free_full (item->key_combos, g_free); item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE); item->editable = g_settings_is_writable (item->settings, item->key); g_object_notify (G_OBJECT (item), "key-combos"); } gboolean cc_keyboard_item_load_from_gsettings_path (CcKeyboardItem *item, const char *path, gboolean reset) { g_autoptr(GVariant) variant = NULL; item->schema = g_strdup (CUSTOM_KEYS_SCHEMA); item->gsettings_path = g_strdup (path); item->key = g_strdup ("binding"); item->settings = g_settings_new_with_path (item->schema, path); item->editable = g_settings_is_writable (item->settings, item->key); item->desc_editable = g_settings_is_writable (item->settings, "name"); item->cmd_editable = g_settings_is_writable (item->settings, "command"); variant = g_settings_get_value (item->settings, item->key); item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY); if (reset) { g_settings_reset (item->settings, "name"); g_settings_reset (item->settings, "command"); g_settings_reset (item->settings, "binding"); } g_settings_bind (item->settings, "name", G_OBJECT (item), "description", G_SETTINGS_BIND_DEFAULT); g_settings_bind (item->settings, "command", G_OBJECT (item), "command", G_SETTINGS_BIND_DEFAULT); g_list_free_full (item->key_combos, g_free); item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE); g_signal_connect_object (G_OBJECT (item->settings), "changed::binding", G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED); return TRUE; } gboolean cc_keyboard_item_load_from_gsettings (CcKeyboardItem *item, const char *description, const char *schema, const char *key) { g_autofree char *signal_name = NULL; g_autoptr(GVariant) variant = NULL; item->schema = g_strdup (schema); item->key = g_strdup (key); item->description = g_strdup (description); item->settings = g_settings_new (item->schema); item->editable = g_settings_is_writable (item->settings, item->key); g_list_free_full (item->key_combos, g_free); item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE); g_list_free_full (item->default_combos, g_free); item->default_combos = settings_get_key_combos (item->settings, item->key, TRUE); variant = g_settings_get_value (item->settings, item->key); item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY); signal_name = g_strdup_printf ("changed::%s", item->key); g_signal_connect_object (G_OBJECT (item->settings), signal_name, G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED); return TRUE; } gboolean cc_keyboard_item_equal (CcKeyboardItem *a, CcKeyboardItem *b) { if (a->type != b->type) return FALSE; switch (a->type) { case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH: return g_str_equal (a->gsettings_path, b->gsettings_path); case CC_KEYBOARD_ITEM_TYPE_GSETTINGS: return (g_str_equal (a->schema, b->schema) && g_str_equal (a->key, b->key)); default: g_assert_not_reached (); } } void cc_keyboard_item_add_reverse_item (CcKeyboardItem *item, CcKeyboardItem *reverse_item, gboolean is_reversed) { g_return_if_fail (item->key != NULL); item->reverse_item = reverse_item; if (reverse_item->reverse_item == NULL) { reverse_item->reverse_item = item; reverse_item->is_reversed = !is_reversed; } else g_warn_if_fail (reverse_item->is_reversed == !!is_reversed); item->is_reversed = !!is_reversed; } CcKeyboardItem * cc_keyboard_item_get_reverse_item (CcKeyboardItem *item) { return item->reverse_item; } void cc_keyboard_item_set_hidden (CcKeyboardItem *item, gboolean hidden) { item->hidden = !!hidden; } gboolean cc_keyboard_item_is_hidden (CcKeyboardItem *item) { return item->hidden; } /** * cc_keyboard_item_is_value_default: * @self: a #CcKeyboardItem * * Retrieves whether the shortcut is the default value or not. * * Returns: %TRUE if the shortcut is the default value, %FALSE otherwise. */ gboolean cc_keyboard_item_is_value_default (CcKeyboardItem *self) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (self), FALSE); /* * When the shortcut is custom, we don't treat it as modified * since we don't know what would be its default value. */ if (self->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) return TRUE; return combos_equal (self->default_combos, self->key_combos); } /** * cc_keyboard_item_reset: * @self: a #CcKeyboardItem * * Reset the keyboard binding to the default value. */ void cc_keyboard_item_reset (CcKeyboardItem *self) { CcKeyboardItem *reverse; g_return_if_fail (CC_IS_KEYBOARD_ITEM (self)); reverse = self->reverse_item; g_settings_reset (self->settings, self->key); g_object_notify (G_OBJECT (self), "is-value-default"); /* Also reset the reverse item */ if (reverse) { g_settings_reset (reverse->settings, reverse->key); g_object_notify (G_OBJECT (reverse), "is-value-default"); } } GList * cc_keyboard_item_get_key_combos (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->key_combos; } GList * cc_keyboard_item_get_default_combos (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->default_combos; } CcKeyCombo cc_keyboard_item_get_primary_combo (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), EMPTY_COMBO); return (item->key_combos) ? *((CcKeyCombo*)item->key_combos->data) : EMPTY_COMBO; } const gchar * cc_keyboard_item_get_key (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->key; } CcKeyboardItemType cc_keyboard_item_get_item_type (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), CC_KEYBOARD_ITEM_TYPE_NONE); return item->type; } const gchar * cc_keyboard_item_get_gsettings_path (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->gsettings_path; } GSettings * cc_keyboard_item_get_settings (CcKeyboardItem *item) { g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); return item->settings; } gboolean cc_keyboard_item_can_set_multiple (CcKeyboardItem *item) { return item->can_set_multiple; } static gchar* combo_get_accelerator (CcKeyCombo *combo) { return gtk_accelerator_name_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask); } static void cc_keyboard_item_add_key_combo_inner (CcKeyboardItem *self, CcKeyCombo *combo) { g_auto(GStrv) strv = NULL; int i; if (!self->can_set_multiple) { g_settings_set_string (self->settings, self->key, combo_get_accelerator (combo)); } else { strv = g_new0 (gchar*, g_list_length (self->key_combos) + 2); i = 0; for (GList *l = self->key_combos; l != NULL; l = l->next, i++) { if (combo_equal (l->data, combo)) // This combo is already in the list return; strv[i] = combo_get_accelerator (l->data); } strv[i] = combo_get_accelerator (combo); g_settings_set_strv (self->settings, self->key, (const gchar **)strv); } binding_changed (self, self->key); } void cc_keyboard_item_add_key_combo (CcKeyboardItem *self, CcKeyCombo *combo) { CcKeyCombo reverse_combo; if (self->reverse_item) { reverse_combo.keyval = combo->keyval; reverse_combo.keycode = combo->keycode; reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK; cc_keyboard_item_add_key_combo_inner (self->reverse_item, &reverse_combo); } cc_keyboard_item_add_key_combo_inner (self, combo); } static void cc_keyboard_item_remove_key_combo_inner (CcKeyboardItem *self, CcKeyCombo *combo) { g_auto(GStrv) strv = NULL; gboolean found; int i; strv = g_new0 (gchar*, g_list_length (self->key_combos) + 1); found = FALSE; i = 0; for (GList *l = self->key_combos; l != NULL; l = l->next, i++) { if (combo_equal (l->data, combo)) { i--; found = TRUE; } else { strv[i] = combo_get_accelerator (l->data); } } if (found) { if (self->can_set_multiple) g_settings_set_strv (self->settings, self->key, (const gchar **)strv); else g_settings_set_string (self->settings, self->key, ""); } binding_changed (self, self->key); } void cc_keyboard_item_remove_key_combo (CcKeyboardItem *self, CcKeyCombo *combo) { CcKeyCombo reverse_combo; if (self->reverse_item) { reverse_combo.keyval = combo->keyval; reverse_combo.keycode = combo->keycode; reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK; cc_keyboard_item_remove_key_combo_inner (self->reverse_item, &reverse_combo); } cc_keyboard_item_remove_key_combo_inner (self, combo); } void cc_keyboard_item_disable (CcKeyboardItem *self) { if (!self->can_set_multiple) { g_settings_set_string (self->settings, self->key, ""); if (self->reverse_item) g_settings_set_string (self->reverse_item->settings, self->reverse_item->key, ""); } else { g_settings_set_strv (self->settings, self->key, NULL); if (self->reverse_item) g_settings_set_strv (self->reverse_item->settings, self->reverse_item->key, NULL); } binding_changed (self, self->key); }