/* -*- 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;
char *binding;
CcKeyboardItem *reverse_item;
gboolean is_reversed;
gboolean hidden;
CcKeyboardItemType type;
CcKeyCombo *primary_combo;
BindingGroupType group;
GtkTreeModel *model;
char *description;
gboolean editable;
GList *key_combos;
GList *default_combos;
/* 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_BINDING,
PROP_EDITABLE,
PROP_TYPE,
PROP_IS_VALUE_DEFAULT,
PROP_COMMAND
};
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 gchar *
get_binding_from_variant (GVariant *variant)
{
const char *str, **strv;
if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING))
return g_variant_get_string (variant, NULL);
else if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY))
return "";
strv = g_variant_get_strv (variant, NULL);
str = strv[0];
g_free (strv);
return str;
}
static gboolean
binding_from_string (const char *str,
CcKeyCombo *combo)
{
g_return_val_if_fail (combo != NULL, FALSE);
guint *keycodes;
if (str == NULL || strcmp (str, "disabled") == 0)
{
memset (combo, 0, sizeof(CcKeyCombo));
return TRUE;
}
gtk_accelerator_parse_with_keycode (str, &combo->keyval, &keycodes, &combo->mask);
combo->keycode = (keycodes ? keycodes[0] : 0);
g_free (keycodes);
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;
}
/* wrapper around g_settings_set_str[ing|v] */
static void
settings_set_binding (GSettings *settings,
const char *key,
const char *value)
{
GVariant *variant;
variant = g_settings_get_value (settings, key);
if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING))
g_settings_set_string (settings, key, value ? value : "");
else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY))
{
if (value == NULL || *value == '\0')
g_settings_set_strv (settings, key, NULL);
else
{
char **str_array = g_new0 (char *, 2);
/* clear any additional bindings by only setting the first one */
*str_array = g_strdup (value);
g_settings_set_strv (settings, key, (const char * const *)str_array);
g_strfreev (str_array);
}
}
g_variant_unref (variant);
}
static void
_set_binding (CcKeyboardItem *item,
const char *value,
gboolean set_backend)
{
CcKeyboardItem *reverse;
gboolean enabled;
reverse = item->reverse_item;
enabled = value && strlen (value) > 0;
g_clear_pointer (&item->binding, g_free);
item->binding = enabled ? g_strdup (value) : g_strdup ("");
binding_from_string (item->binding, item->primary_combo);
/*
* Always treat the pair (item, reverse) as a unit: setting one also
* disables the other, disabling one up also sets the other.
*/
if (reverse)
{
GdkModifierType reverse_mask;
reverse_mask = enabled ? item->primary_combo->mask ^ GDK_SHIFT_MASK
: item->primary_combo->mask;
g_clear_pointer (&reverse->binding, g_free);
if (enabled)
reverse->binding = gtk_accelerator_name_with_keycode (NULL,
item->primary_combo->keyval,
item->primary_combo->keycode,
reverse_mask);
binding_from_string (reverse->binding, reverse->primary_combo);
}
if (set_backend == FALSE)
return;
settings_set_binding (item->settings, item->key, item->binding);
g_object_notify (G_OBJECT (item), "is-value-default");
if (reverse)
{
settings_set_binding (reverse->settings, reverse->key, reverse->binding);
g_object_notify (G_OBJECT (reverse), "is-value-default");
}
}
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_BINDING:
_set_binding (self, g_value_get_string (value), TRUE);
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_BINDING:
g_value_set_string (value, self->binding);
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;
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_BINDING,
g_param_spec_string ("binding",
"binding",
"binding",
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));
}
static void
cc_keyboard_item_init (CcKeyboardItem *item)
{
item->primary_combo = g_new0 (CcKeyCombo, 1);
}
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->binding);
g_free (item->primary_combo);
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)
{
GdkKeymap *keymap = gdk_keymap_get_for_display (gdk_display_get_default ());
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_keymap_get_entries_for_keycode (keymap, 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 char *
translate_binding_string (const char *str)
{
g_autofree guint *above_tab_keysyms = NULL;
g_autofree char *symname = NULL;
g_auto (GStrv) split_str = NULL;
if (str == NULL || strstr (str, "Above_Tab") == NULL)
return g_strdup (str);
above_tab_keysyms = get_above_tab_keysyms ();
symname = gtk_accelerator_name (above_tab_keysyms[0], 0);
split_str = g_strsplit (str, "Above_Tab", -1);
return g_strjoinv (symname, split_str);
}
static GList *
variant_get_key_combos (GVariant *variant)
{
GList *combos = NULL;
char **bindings = NULL, **translated_bindings, **str;
if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING))
{
bindings = g_malloc0_n (2, sizeof(char *));
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++)
{
CcKeyCombo *combo = g_new (CcKeyCombo, 1);
binding_from_string (*str, combo);
combos = g_list_prepend (combos, combo);
}
g_strfreev (bindings);
return g_list_reverse (combos);
}
static GList *
settings_get_key_combos (GSettings *settings,
const char *key,
gboolean use_default)
{
GList *key_combos;
GVariant *variant;
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);
g_variant_unref (variant);
return key_combos;
}
/* wrapper around g_settings_get_str[ing|v] */
static char *
settings_get_binding (GSettings *settings,
const char *key)
{
GVariant *variant;
char *value = NULL;
variant = g_settings_get_value (settings, key);
if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING))
value = translate_binding_string (g_variant_get_string (variant, NULL));
else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY))
{
const char **str_array;
str_array = g_variant_get_strv (variant, NULL);
value = translate_binding_string (str_array[0]);
g_free (str_array);
}
g_variant_unref (variant);
return value;
}
static void
binding_changed (GSettings *settings,
const char *key,
CcKeyboardItem *item)
{
char *value;
g_list_free_full (item->key_combos, g_free);
item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);
value = settings_get_binding (item->settings, item->key);
item->editable = g_settings_is_writable (item->settings, item->key);
_set_binding (item, value, FALSE);
g_free (value);
g_object_notify (G_OBJECT (item), "binding");
}
gboolean
cc_keyboard_item_load_from_gsettings_path (CcKeyboardItem *item,
const char *path,
gboolean reset)
{
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");
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_free (item->binding);
item->binding = settings_get_binding (item->settings, item->key);
binding_from_string (item->binding, item->primary_combo);
g_signal_connect (G_OBJECT (item->settings), "changed::binding",
G_CALLBACK (binding_changed), item);
return TRUE;
}
gboolean
cc_keyboard_item_load_from_gsettings (CcKeyboardItem *item,
const char *description,
const char *schema,
const char *key)
{
char *signal_name;
item->schema = g_strdup (schema);
item->key = g_strdup (key);
item->description = g_strdup (description);
item->settings = g_settings_new (item->schema);
g_free (item->binding);
item->binding = settings_get_binding (item->settings, item->key);
item->editable = g_settings_is_writable (item->settings, item->key);
binding_from_string (item->binding, item->primary_combo);
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);
signal_name = g_strdup_printf ("changed::%s", item->key);
g_signal_connect (G_OBJECT (item->settings), signal_name,
G_CALLBACK (binding_changed), item);
g_free (signal_name);
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)
{
GVariant *user_value;
gboolean is_value_default;
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;
user_value = g_settings_get_user_value (self->settings, self->key);
is_value_default = TRUE;
if (user_value)
{
GVariant *default_value;
const gchar *default_binding, *user_binding;
default_value = g_settings_get_default_value (self->settings, self->key);
default_binding = get_binding_from_variant (default_value);
user_binding = get_binding_from_variant (user_value);
is_value_default = (g_strcmp0 (default_binding, user_binding) == 0);
g_clear_pointer (&default_value, g_variant_unref);
}
g_clear_pointer (&user_value, g_variant_unref);
return is_value_default;
}
/**
* 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), NULL);
return item->primary_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;
}
void
cc_keyboard_item_set_model (CcKeyboardItem *item, GtkTreeModel *model, BindingGroupType group)
{
g_return_if_fail (CC_IS_KEYBOARD_ITEM (item));
item->model = model;
item->group = group;
}
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;
}