/*
* Copyright (C) 2010 Intel, Inc
* Copyright (C) 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 .
*
* Authors: Thomas Wood
* Rodrigo Moya
* Christophe Fergeau
*/
#include
#include
#include "keyboard-shortcuts.h"
#define CUSTOM_KEYS_BASENAME "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings"
static char *
replace_pictures_folder (const char *description)
{
g_autoptr(GRegex) pictures_regex = NULL;
const char *path;
g_autofree gchar *dirname = NULL;
g_autofree gchar *ret = NULL;
if (description == NULL)
return NULL;
if (strstr (description, "$PICTURES") == NULL)
return g_strdup (description);
pictures_regex = g_regex_new ("\\$PICTURES", 0, 0, NULL);
path = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES);
dirname = g_filename_display_basename (path);
ret = g_regex_replace (pictures_regex, description, -1,
0, dirname, 0, NULL);
if (ret == NULL)
return g_strdup (description);
return g_steal_pointer (&ret);
}
static void
parse_start_tag (GMarkupParseContext *ctx,
const gchar *element_name,
const gchar **attr_names,
const gchar **attr_values,
gpointer user_data,
GError **error)
{
KeyList *keylist = (KeyList *) user_data;
KeyListEntry key;
const char *name, *schema, *description, *package, *context, *orig_description, *reverse_entry;
gboolean is_reversed, hidden;
name = NULL;
schema = NULL;
package = NULL;
context = NULL;
/* The top-level element, names the section in the tree */
if (g_str_equal (element_name, "KeyListEntries"))
{
const char *wm_name = NULL;
const char *group = NULL;
while (*attr_names && *attr_values)
{
if (g_str_equal (*attr_names, "name"))
{
if (**attr_values)
name = *attr_values;
} else if (g_str_equal (*attr_names, "group")) {
if (**attr_values)
group = *attr_values;
} else if (g_str_equal (*attr_names, "wm_name")) {
if (**attr_values)
wm_name = *attr_values;
} else if (g_str_equal (*attr_names, "schema")) {
if (**attr_values)
schema = *attr_values;
} else if (g_str_equal (*attr_names, "package")) {
if (**attr_values)
package = *attr_values;
}
++attr_names;
++attr_values;
}
if (name)
{
if (keylist->name)
g_warning ("Duplicate section name");
g_free (keylist->name);
keylist->name = g_strdup (name);
}
if (wm_name)
{
if (keylist->wm_name)
g_warning ("Duplicate window manager name");
g_free (keylist->wm_name);
keylist->wm_name = g_strdup (wm_name);
}
if (package)
{
if (keylist->package)
g_warning ("Duplicate gettext package name");
g_free (keylist->package);
keylist->package = g_strdup (package);
bind_textdomain_codeset (keylist->package, "UTF-8");
}
if (group)
{
if (keylist->group)
g_warning ("Duplicate group");
g_free (keylist->group);
keylist->group = g_strdup (group);
}
if (schema)
{
if (keylist->schema)
g_warning ("Duplicate schema");
g_free (keylist->schema);
keylist->schema = g_strdup (schema);
}
return;
}
if (!g_str_equal (element_name, "KeyListEntry")
|| attr_names == NULL
|| attr_values == NULL)
return;
schema = NULL;
description = NULL;
context = NULL;
orig_description = NULL;
reverse_entry = NULL;
is_reversed = FALSE;
hidden = FALSE;
while (*attr_names && *attr_values)
{
if (g_str_equal (*attr_names, "name"))
{
/* skip if empty */
if (**attr_values)
name = *attr_values;
} else if (g_str_equal (*attr_names, "schema")) {
if (**attr_values) {
schema = *attr_values;
}
} else if (g_str_equal (*attr_names, "description")) {
if (**attr_values)
orig_description = *attr_values;
} else if (g_str_equal (*attr_names, "msgctxt")) {
if (**attr_values)
context = *attr_values;
} else if (g_str_equal (*attr_names, "reverse-entry")) {
if (**attr_values)
reverse_entry = *attr_values;
} else if (g_str_equal (*attr_names, "is-reversed")) {
if (g_str_equal (*attr_values, "true"))
is_reversed = TRUE;
} else if (g_str_equal (*attr_names, "hidden")) {
if (g_str_equal (*attr_values, "true"))
hidden = TRUE;
}
++attr_names;
++attr_values;
}
if (name == NULL)
return;
if (schema == NULL &&
keylist->schema == NULL) {
g_debug ("Ignored GConf keyboard shortcut '%s'", name);
return;
}
if (context != NULL)
description = g_dpgettext2 (keylist->package, context, orig_description);
else
description = dgettext (keylist->package, orig_description);
key.name = g_strdup (name);
key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS;
key.description = replace_pictures_folder (description);
key.schema = schema ? g_strdup (schema) : g_strdup (keylist->schema);
key.reverse_entry = g_strdup (reverse_entry);
key.is_reversed = is_reversed;
key.hidden = hidden;
g_array_append_val (keylist->entries, key);
}
static const guint forbidden_keyvals[] = {
/* Navigation keys */
GDK_KEY_Home,
GDK_KEY_Left,
GDK_KEY_Up,
GDK_KEY_Right,
GDK_KEY_Down,
GDK_KEY_Page_Up,
GDK_KEY_Page_Down,
GDK_KEY_End,
GDK_KEY_Tab,
/* Return */
GDK_KEY_KP_Enter,
GDK_KEY_Return,
GDK_KEY_Mode_switch
};
static gboolean
keyval_is_forbidden (guint keyval)
{
guint i;
for (i = 0; i < G_N_ELEMENTS(forbidden_keyvals); i++) {
if (keyval == forbidden_keyvals[i])
return TRUE;
}
return FALSE;
}
gboolean
is_valid_binding (const CcKeyCombo *combo)
{
if ((combo->mask == 0 || combo->mask == GDK_SHIFT_MASK) && combo->keycode != 0)
{
guint keyval = combo->keyval;
if ((keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
|| (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z)
|| (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
|| (keyval >= GDK_KEY_kana_fullstop && keyval <= GDK_KEY_semivoicedsound)
|| (keyval >= GDK_KEY_Arabic_comma && keyval <= GDK_KEY_Arabic_sukun)
|| (keyval >= GDK_KEY_Serbian_dje && keyval <= GDK_KEY_Cyrillic_HARDSIGN)
|| (keyval >= GDK_KEY_Greek_ALPHAaccent && keyval <= GDK_KEY_Greek_omega)
|| (keyval >= GDK_KEY_hebrew_doublelowline && keyval <= GDK_KEY_hebrew_taf)
|| (keyval >= GDK_KEY_Thai_kokai && keyval <= GDK_KEY_Thai_lekkao)
|| (keyval >= GDK_KEY_Hangul_Kiyeog && keyval <= GDK_KEY_Hangul_J_YeorinHieuh)
|| (keyval == GDK_KEY_space && combo->mask == 0)
|| keyval_is_forbidden (keyval)) {
return FALSE;
}
}
return TRUE;
}
gboolean
is_empty_binding (const CcKeyCombo *combo)
{
if (combo->keyval == 0 &&
combo->mask == 0 &&
combo->keycode == 0)
return TRUE;
return FALSE;
}
gboolean
is_valid_accel (const CcKeyCombo *combo)
{
/* Unlike gtk_accelerator_valid(), we want to allow Tab when combined
* with some modifiers (Alt+Tab and friends)
*/
return gtk_accelerator_valid (combo->keyval, combo->mask) ||
(combo->keyval == GDK_KEY_Tab && combo->mask != 0);
}
gchar*
find_free_settings_path (GSettings *settings)
{
g_auto(GStrv) used_names = NULL;
g_autofree gchar *dir = NULL;
int i, num, n_names;
used_names = g_settings_get_strv (settings, "custom-keybindings");
n_names = g_strv_length (used_names);
for (num = 0; dir == NULL; num++)
{
g_autofree gchar *tmp = NULL;
gboolean found = FALSE;
tmp = g_strdup_printf ("%s/custom%d/", CUSTOM_KEYS_BASENAME, num);
for (i = 0; i < n_names && !found; i++)
found = strcmp (used_names[i], tmp) == 0;
if (!found)
dir = g_steal_pointer (&tmp);
}
return g_steal_pointer (&dir);
}
KeyList*
parse_keylist_from_file (const gchar *path)
{
KeyList *keylist;
g_autoptr(GError) err = NULL;
g_autofree gchar *buf = NULL;
gsize buf_len;
guint i;
g_autoptr(GMarkupParseContext) ctx = NULL;
GMarkupParser parser = { parse_start_tag, NULL, NULL, NULL, NULL };
/* Parse file */
if (!g_file_get_contents (path, &buf, &buf_len, &err))
return NULL;
keylist = g_new0 (KeyList, 1);
keylist->entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry));
ctx = g_markup_parse_context_new (&parser, 0, keylist, NULL);
if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err))
{
g_warning ("Failed to parse '%s': '%s'", path, err->message);
g_free (keylist->name);
g_free (keylist->package);
g_free (keylist->wm_name);
for (i = 0; i < keylist->entries->len; i++)
g_free (((KeyListEntry *) &(keylist->entries->data[i]))->name);
g_array_free (keylist->entries, TRUE);
g_free (keylist);
return NULL;
}
return keylist;
}
/*
* Stolen from GtkCellRendererAccel:
* https://git.gnome.org/browse/gtk+/tree/gtk/gtkcellrendereraccel.c#n261
*/
gchar*
convert_keysym_state_to_string (const CcKeyCombo *combo)
{
gchar *name;
if (combo->keyval == 0 && combo->keycode == 0)
{
/* This label is displayed in a treeview cell displaying
* a disabled accelerator key combination.
*/
name = g_strdup (_("Disabled"));
}
else
{
name = gtk_accelerator_get_label_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask);
if (name == NULL)
name = gtk_accelerator_name_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask);
}
return name;
}
/* This adjusts the keyval and modifiers such that it matches how
* gnome-shell detects shortcuts, which works as follows:
* First for the non-modifier key, the keycode that generates this
* keyval at the lowest shift level is determined, which might be a
* level > 0, such as for numbers in the num-row in AZERTY.
* Next it checks if all the specified modifiers were pressed.
*/
void
normalize_keyval_and_mask (guint keycode,
GdkModifierType mask,
guint group,
guint *out_keyval,
GdkModifierType *out_mask)
{
guint unmodified_keyval;
guint shifted_keyval;
GdkModifierType explicit_modifiers;
GdkModifierType used_modifiers;
/* We want shift to always be included as explicit modifier for
* gnome-shell shortcuts. That's because users usually think of
* shortcuts as including the shift key rather than being defined
* for the shifted keyval.
* This helps with num-row keys which have different keyvals on
* different layouts for example, but also with keys that have
* explicit key codes at shift level 0, that gnome-shell would prefer
* over shifted ones, such the DOLLAR key.
*/
explicit_modifiers = gtk_accelerator_get_default_mod_mask () | GDK_SHIFT_MASK;
used_modifiers = mask & explicit_modifiers;
/* Find the base keyval of the pressed key without the explicit
* modifiers. */
gdk_display_translate_key (gdk_display_get_default (),
keycode,
mask & ~explicit_modifiers,
group,
&unmodified_keyval,
NULL,
NULL,
NULL);
/* Normalize num-row keys to the number value. This allows these
* shortcuts to work when switching between AZERTY and layouts where
* the numbers are at shift level 0. */
gdk_display_translate_key (gdk_display_get_default (),
keycode,
GDK_SHIFT_MASK | (mask & ~explicit_modifiers),
group,
&shifted_keyval,
NULL,
NULL,
NULL);
if (shifted_keyval >= GDK_KEY_0 && shifted_keyval <= GDK_KEY_9)
unmodified_keyval = shifted_keyval;
/* Normalise */
if (unmodified_keyval == GDK_KEY_ISO_Left_Tab)
unmodified_keyval = GDK_KEY_Tab;
if (unmodified_keyval == GDK_KEY_Sys_Req && (used_modifiers & GDK_ALT_MASK) != 0)
{
/* HACK: we don't want to use SysRq as a keybinding (but we do
* want Alt+Print), so we avoid translation from Alt+Print to SysRq */
unmodified_keyval = GDK_KEY_Print;
}
*out_keyval = unmodified_keyval;
*out_mask = used_modifiers;
}