diff options
Diffstat (limited to 'panels/keyboard')
30 files changed, 6596 insertions, 0 deletions
diff --git a/panels/keyboard/00-multimedia.xml.in b/panels/keyboard/00-multimedia.xml.in new file mode 100644 index 0000000..bb1532d --- /dev/null +++ b/panels/keyboard/00-multimedia.xml.in @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<KeyListEntries group="system" schema="org.gnome.settings-daemon.plugins.media-keys" name="Sound and Media"> + + <KeyListEntry name="volume-mute" description="Volume mute/unmute"/> + + <KeyListEntry name="volume-down" description="Volume down"/> + + <KeyListEntry name="volume-up" description="Volume up"/> + + <KeyListEntry name="mic-mute" description="Microphone mute/unmute"/> + + <KeyListEntry name="media" description="Launch media player"/> + + <KeyListEntry name="play" description="Play (or play/pause)"/> + + <KeyListEntry name="pause" description="Pause playback"/> + + <KeyListEntry name="stop" description="Stop playback"/> + + <KeyListEntry name="previous" description="Previous track"/> + + <KeyListEntry name="next" description="Next track"/> + + <KeyListEntry name="eject" description="Eject"/> + +</KeyListEntries> + diff --git a/panels/keyboard/01-input-sources.xml.in b/panels/keyboard/01-input-sources.xml.in new file mode 100644 index 0000000..355bba4 --- /dev/null +++ b/panels/keyboard/01-input-sources.xml.in @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<KeyListEntries group="system" + schema="org.gnome.desktop.wm.keybindings" + name="Typing"> + + <KeyListEntry name="switch-input-source" + reverse-entry="switch-input-source-backward" + description="Switch to next input source"/> + + <KeyListEntry name="switch-input-source-backward" + reverse-entry="switch-input-source" + is-reversed="true" + description="Switch to previous input source"/> + +</KeyListEntries> diff --git a/panels/keyboard/01-launchers.xml.in b/panels/keyboard/01-launchers.xml.in new file mode 100644 index 0000000..67c8325 --- /dev/null +++ b/panels/keyboard/01-launchers.xml.in @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<KeyListEntries group="system" schema="org.gnome.settings-daemon.plugins.media-keys" name="Launchers"> + + <KeyListEntry name="help" description="Launch help browser"/> + + <KeyListEntry name="control-center" description="Settings"/> + + <KeyListEntry name="calculator" description="Launch calculator"/> + + <KeyListEntry name="email" description="Launch email client"/> + + <KeyListEntry name="www" description="Launch web browser"/> + + <KeyListEntry name="home" description="Home folder"/> + + <KeyListEntry name="search" description="Search" msgctxt="keybinding">Search</KeyListEntry> + +</KeyListEntries> + diff --git a/panels/keyboard/01-screenshot.xml.in b/panels/keyboard/01-screenshot.xml.in new file mode 100644 index 0000000..002bd38 --- /dev/null +++ b/panels/keyboard/01-screenshot.xml.in @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<KeyListEntries group="system" schema="org.gnome.settings-daemon.plugins.media-keys" name="Screenshots"> + + <!-- translators: $PICTURES will be replaced by the name of the XDG Pictures directory --> + <KeyListEntry name="screenshot" + description="Save a screenshot to $PICTURES"/> + + <!-- translators: $PICTURES will be replaced by the name of the XDG Pictures directory --> + <KeyListEntry name="window-screenshot" + description="Save a screenshot of a window to $PICTURES"/> + + <!-- translators: $PICTURES will be replaced by the name of the XDG Pictures directory --> + <KeyListEntry name="area-screenshot" + description="Save a screenshot of an area to $PICTURES"/> + + <KeyListEntry name="screenshot-clip" + description="Copy a screenshot to clipboard"/> + + <KeyListEntry name="window-screenshot-clip" + description="Copy a screenshot of a window to clipboard"/> + + <KeyListEntry name="area-screenshot-clip" + description="Copy a screenshot of an area to clipboard"/> + + <KeyListEntry name="screencast" + description="Record a short screencast"/> + +</KeyListEntries> + diff --git a/panels/keyboard/01-system.xml.in b/panels/keyboard/01-system.xml.in new file mode 100644 index 0000000..1fcf78b --- /dev/null +++ b/panels/keyboard/01-system.xml.in @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<KeyListEntries group="system" schema="org.gnome.settings-daemon.plugins.media-keys" name="System"> + + <KeyListEntry name="logout" description="Log out"/> + + <KeyListEntry name="screensaver" description="Lock screen"/> + +</KeyListEntries> + diff --git a/panels/keyboard/50-accessibility.xml.in b/panels/keyboard/50-accessibility.xml.in new file mode 100644 index 0000000..34bcb04 --- /dev/null +++ b/panels/keyboard/50-accessibility.xml.in @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<KeyListEntries group="system" name="Accessibility" schema="org.gnome.settings-daemon.plugins.media-keys"> + + <KeyListEntry name="magnifier" description="Turn zoom on or off"/> + + <KeyListEntry name="magnifier-zoom-in" description="Zoom in"/> + + <KeyListEntry name="magnifier-zoom-out" description="Zoom out"/> + + <KeyListEntry name="screenreader" description="Turn screen reader on or off"/> + + <KeyListEntry name="on-screen-keyboard" description="Turn on-screen keyboard on or off"/> + + <KeyListEntry name="increase-text-size" description="Increase text size"/> + + <KeyListEntry name="decrease-text-size" description="Decrease text size"/> + + <KeyListEntry name="toggle-contrast" description="High contrast on or off"/> + +</KeyListEntries> diff --git a/panels/keyboard/cc-alt-chars-key-dialog.c b/panels/keyboard/cc-alt-chars-key-dialog.c new file mode 100644 index 0000000..c32c6ba --- /dev/null +++ b/panels/keyboard/cc-alt-chars-key-dialog.c @@ -0,0 +1,209 @@ +/* cc-alt-chars-key-dialog.c + * + * Copyright 2019 Bastien Nocera <hadess@hadess.net> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "cc-alt-chars-key-dialog.h" + +struct _CcAltCharsKeyDialog +{ + GtkDialog parent_instance; + + GSettings *input_source_settings; + + GtkRadioButton *leftalt_radio; + GtkRadioButton *leftsuper_radio; + GtkRadioButton *menukey_radio; + GtkRadioButton *rightalt_radio; + GtkRadioButton *rightctrl_radio; + GtkRadioButton *rightsuper_radio; +}; + +G_DEFINE_TYPE (CcAltCharsKeyDialog, cc_alt_chars_key_dialog, GTK_TYPE_DIALOG) + +static GtkRadioButton * +get_radio_button_from_xkb_option_name (CcAltCharsKeyDialog *self, + const gchar *name) +{ + if (g_str_equal (name, "lv3:switch")) + return self->rightctrl_radio; + else if (g_str_equal (name, "lv3:menu_switch")) + return self->menukey_radio; + else if (g_str_equal (name, "lv3:lwin_switch")) + return self->leftsuper_radio; + else if (g_str_equal (name, "lv3:rwin_switch")) + return self->rightsuper_radio; + else if (g_str_equal (name, "lv3:lalt_switch")) + return self->leftalt_radio; + else if (g_str_equal (name, "lv3:ralt_switch")) + return self->rightalt_radio; + + return NULL; +} + +static const gchar * +get_xkb_option_name_from_radio_button (CcAltCharsKeyDialog *self, + GtkRadioButton *radio) +{ + if (radio == self->rightctrl_radio) + return "lv3:switch"; + else if (radio == self->menukey_radio) + return "lv3:menu_switch"; + else if (radio == self->leftsuper_radio) + return "lv3:lwin_switch"; + else if (radio == self->rightsuper_radio) + return "lv3:rwin_switch"; + else if (radio == self->leftalt_radio) + return "lv3:lalt_switch"; + else if (radio == self->rightalt_radio) + return "lv3:ralt_switch"; + + return NULL; +} + +static void +update_active_radio (CcAltCharsKeyDialog *self) +{ + g_auto(GStrv) options = NULL; + guint i; + + options = g_settings_get_strv (self->input_source_settings, "xkb-options"); + + for (i = 0; options != NULL && options[i] != NULL; i++) + { + GtkRadioButton *radio; + + if (!g_str_has_prefix (options[i], "lv3:")) + continue; + + radio = get_radio_button_from_xkb_option_name (self, options[i]); + + if (!radio) + continue; + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (radio), TRUE); + return; + } + + /* Fallback to Right Alt as default */ + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->rightalt_radio), TRUE); +} + +static void +on_active_lv3_changed_cb (GtkRadioButton *radio, + CcAltCharsKeyDialog *self) +{ + g_autoptr(GPtrArray) array = NULL; + g_auto(GStrv) options = NULL; + gboolean found; + guint i; + + if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (radio))) + return; + + /* Either replace the existing "lv3:" option in the string + * array, or add the option at the end + */ + array = g_ptr_array_new (); + options = g_settings_get_strv (self->input_source_settings, "xkb-options"); + found = FALSE; + + for (i = 0; options != NULL && options[i] != NULL; i++) + { + if (g_str_has_prefix (options[i], "lv3:")) + { + g_ptr_array_add (array, (gchar *)get_xkb_option_name_from_radio_button (self, radio)); + found = TRUE; + } + else + { + g_ptr_array_add (array, options[i]); + } + } + + if (!found) + g_ptr_array_add (array, (gchar *)get_xkb_option_name_from_radio_button (self, radio)); + + g_ptr_array_add (array, NULL); + + g_settings_set_strv (self->input_source_settings, + "xkb-options", + (const gchar * const *) array->pdata); +} + +static void +on_xkb_options_changed_cb (CcAltCharsKeyDialog *self) +{ + update_active_radio (self); +} + +static void +cc_alt_chars_key_dialog_finalize (GObject *object) +{ + CcAltCharsKeyDialog *self = (CcAltCharsKeyDialog *)object; + + g_clear_object (&self->input_source_settings); + + G_OBJECT_CLASS (cc_alt_chars_key_dialog_parent_class)->finalize (object); +} + +static void +cc_alt_chars_key_dialog_class_init (CcAltCharsKeyDialogClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cc_alt_chars_key_dialog_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-alt-chars-key-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcAltCharsKeyDialog, leftalt_radio); + gtk_widget_class_bind_template_child (widget_class, CcAltCharsKeyDialog, leftsuper_radio); + gtk_widget_class_bind_template_child (widget_class, CcAltCharsKeyDialog, menukey_radio); + gtk_widget_class_bind_template_child (widget_class, CcAltCharsKeyDialog, rightalt_radio); + gtk_widget_class_bind_template_child (widget_class, CcAltCharsKeyDialog, rightctrl_radio); + gtk_widget_class_bind_template_child (widget_class, CcAltCharsKeyDialog, rightsuper_radio); + + gtk_widget_class_bind_template_callback (widget_class, on_active_lv3_changed_cb); +} + +static void +cc_alt_chars_key_dialog_init (CcAltCharsKeyDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->input_source_settings = g_settings_new ("org.gnome.desktop.input-sources"); + g_signal_connect_object (self->input_source_settings, + "changed::xkb-options", + G_CALLBACK (on_xkb_options_changed_cb), + self, G_CONNECT_SWAPPED); + update_active_radio (self); +} + +CcAltCharsKeyDialog * +cc_alt_chars_key_dialog_new (GSettings *input_settings) +{ + CcAltCharsKeyDialog *self; + + self = g_object_new (CC_TYPE_ALT_CHARS_KEY_DIALOG, + "use-header-bar", 1, + NULL); + self->input_source_settings = g_object_ref (input_settings); + + return self; +} diff --git a/panels/keyboard/cc-alt-chars-key-dialog.h b/panels/keyboard/cc-alt-chars-key-dialog.h new file mode 100644 index 0000000..fb0c853 --- /dev/null +++ b/panels/keyboard/cc-alt-chars-key-dialog.h @@ -0,0 +1,32 @@ +/* cc-alt-chars-key-dialog.h + * + * Copyright 2019 Bastien Nocera <hadess@hadess.net> + * + * 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 <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_ALT_CHARS_KEY_DIALOG (cc_alt_chars_key_dialog_get_type()) +G_DECLARE_FINAL_TYPE (CcAltCharsKeyDialog, cc_alt_chars_key_dialog, CC, ALT_CHARS_KEY_DIALOG, GtkDialog) + +CcAltCharsKeyDialog *cc_alt_chars_key_dialog_new (GSettings *input_settings); + +G_END_DECLS diff --git a/panels/keyboard/cc-alt-chars-key-dialog.ui b/panels/keyboard/cc-alt-chars-key-dialog.ui new file mode 100644 index 0000000..67d7206 --- /dev/null +++ b/panels/keyboard/cc-alt-chars-key-dialog.ui @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="CcAltCharsKeyDialog" parent="GtkDialog"> + <property name="modal">True</property> + <property name="can_focus">False</property> + <property name="resizable">False</property> + <property name="type_hint">dialog</property> + <property name="title" translatable="yes">Alternate Characters Key</property> + <signal name="delete-event" handler="gtk_widget_hide_on_delete" /> + <child internal-child="vbox"> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin">18</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">6</property> + <property name="margin_bottom">12</property> + <property name="label" translatable="yes">The alternate characters key can be used to enter additional characters. These are sometimes printed as a third-option on your keyboard.</property> + <property name="wrap">True</property> + <property name="width_chars">40</property> + <property name="max_width_chars">40</property> + </object> + </child> + + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">6</property> + <property name="column_spacing">6</property> + <property name="column_homogeneous">True</property> + + <child> + <object class="GtkRadioButton" id="leftalt_radio"> + <property name="label" translatable="yes">Left Alt</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + <signal name="toggled" handler="on_active_lv3_changed_cb" object="CcAltCharsKeyDialog" swapped="no" /> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + + <child> + <object class="GtkRadioButton" id="rightalt_radio"> + <property name="label" translatable="yes">Right Alt</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <property name="group">leftalt_radio</property> + <signal name="toggled" handler="on_active_lv3_changed_cb" object="CcAltCharsKeyDialog" swapped="no" /> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + + <child> + <object class="GtkRadioButton" id="leftsuper_radio"> + <property name="label" translatable="yes">Left Super</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <property name="group">leftalt_radio</property> + <signal name="toggled" handler="on_active_lv3_changed_cb" object="CcAltCharsKeyDialog" swapped="no" /> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + <object class="GtkRadioButton" id="rightsuper_radio"> + <property name="label" translatable="yes">Right Super</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <property name="group">leftalt_radio</property> + <signal name="toggled" handler="on_active_lv3_changed_cb" object="CcAltCharsKeyDialog" swapped="no" /> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + + <child> + <object class="GtkRadioButton" id="menukey_radio"> + <property name="label" translatable="yes">Menu key</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <property name="group">leftalt_radio</property> + <signal name="toggled" handler="on_active_lv3_changed_cb" object="CcAltCharsKeyDialog" swapped="no" /> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + + <child> + <object class="GtkRadioButton" id="rightctrl_radio"> + <property name="label" translatable="yes">Right Ctrl</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + <property name="group">leftalt_radio</property> + <signal name="toggled" handler="on_active_lv3_changed_cb" object="CcAltCharsKeyDialog" swapped="no" /> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + + </object> + </child> + + </object> + </child> + + </object> + </child> + + <child internal-child="headerbar"> + <object class="GtkHeaderBar"> + <property name="can_focus">False</property> + <property name="show_close_button">True</property> + </object> + </child> + </template> +</interface> diff --git a/panels/keyboard/cc-keyboard-item.c b/panels/keyboard/cc-keyboard-item.c new file mode 100644 index 0000000..853c598 --- /dev/null +++ b/panels/keyboard/cc-keyboard-item.c @@ -0,0 +1,874 @@ +/* -*- 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 <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> + +#include <gtk/gtk.h> +#include <gio/gio.h> +#include <glib/gi18n-lib.h> + +#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; + 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 (CcKeyboardItem *item, + const char *key) +{ + 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_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) +{ + 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_object (G_OBJECT (item->settings), signal_name, + G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED); + 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; +} + +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; +} + diff --git a/panels/keyboard/cc-keyboard-item.h b/panels/keyboard/cc-keyboard-item.h new file mode 100644 index 0000000..2c344a7 --- /dev/null +++ b/panels/keyboard/cc-keyboard-item.h @@ -0,0 +1,103 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (C) 2011 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 <http://www.gnu.org/licenses/>. + * + */ + +#pragma once + +#include <glib-object.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_KEYBOARD_ITEM (cc_keyboard_item_get_type ()) +G_DECLARE_FINAL_TYPE (CcKeyboardItem, cc_keyboard_item, CC, KEYBOARD_ITEM, GObject) + +typedef enum +{ + BINDING_GROUP_SYSTEM, + BINDING_GROUP_APPS, + BINDING_GROUP_SEPARATOR, + BINDING_GROUP_USER, +} BindingGroupType; + +typedef enum +{ + CC_KEYBOARD_ITEM_TYPE_NONE = 0, + CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH, + CC_KEYBOARD_ITEM_TYPE_GSETTINGS +} CcKeyboardItemType; + +typedef struct +{ + guint keyval; + guint keycode; + GdkModifierType mask; +} CcKeyCombo; + +CcKeyboardItem* cc_keyboard_item_new (CcKeyboardItemType type); + +gboolean cc_keyboard_item_load_from_gsettings_path (CcKeyboardItem *item, + const char *path, + gboolean reset); + +gboolean cc_keyboard_item_load_from_gsettings (CcKeyboardItem *item, + const char *description, + const char *schema, + const char *key); + +const char* cc_keyboard_item_get_description (CcKeyboardItem *item); + +gboolean cc_keyboard_item_get_desc_editable (CcKeyboardItem *item); + +const char* cc_keyboard_item_get_command (CcKeyboardItem *item); + +gboolean cc_keyboard_item_get_cmd_editable (CcKeyboardItem *item); + +gboolean cc_keyboard_item_equal (CcKeyboardItem *a, + CcKeyboardItem *b); + +void cc_keyboard_item_add_reverse_item (CcKeyboardItem *item, + CcKeyboardItem *reverse_item, + gboolean is_reversed); + +CcKeyboardItem* cc_keyboard_item_get_reverse_item (CcKeyboardItem *item); + +void cc_keyboard_item_set_hidden (CcKeyboardItem *item, + gboolean hidden); + +gboolean cc_keyboard_item_is_hidden (CcKeyboardItem *item); + +gboolean cc_keyboard_item_is_value_default (CcKeyboardItem *self); + +void cc_keyboard_item_reset (CcKeyboardItem *self); + +GList* cc_keyboard_item_get_key_combos (CcKeyboardItem *self); + +GList* cc_keyboard_item_get_default_combos (CcKeyboardItem *self); + +CcKeyCombo* cc_keyboard_item_get_primary_combo (CcKeyboardItem *self); + +const gchar* cc_keyboard_item_get_key (CcKeyboardItem *self); + +CcKeyboardItemType cc_keyboard_item_get_item_type (CcKeyboardItem *self); + +const gchar* cc_keyboard_item_get_gsettings_path (CcKeyboardItem *self); + +GSettings* cc_keyboard_item_get_settings (CcKeyboardItem *self); + +G_END_DECLS diff --git a/panels/keyboard/cc-keyboard-manager.c b/panels/keyboard/cc-keyboard-manager.c new file mode 100644 index 0000000..55ae3af --- /dev/null +++ b/panels/keyboard/cc-keyboard-manager.c @@ -0,0 +1,923 @@ +/* + * Copyright (C) 2010 Intel, Inc + * Copyright (C) 2016 Endless, 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 <http://www.gnu.org/licenses/>. + * + * Author: Thomas Wood <thomas.wood@intel.com> + * Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + */ + +#include <glib/gi18n.h> + +#include "cc-keyboard-manager.h" +#include "keyboard-shortcuts.h" +#include "wm-common.h" + +#ifdef GDK_WINDOWING_X11 +#include <gdk/gdkx.h> +#endif + +#define BINDINGS_SCHEMA "org.gnome.settings-daemon.plugins.media-keys" +#define CUSTOM_SHORTCUTS_ID "custom" + +struct _CcKeyboardManager +{ + GObject parent; + + GtkListStore *sections_store; + + GHashTable *kb_system_sections; + GHashTable *kb_apps_sections; + GHashTable *kb_user_sections; + + GSettings *binding_settings; + + gpointer wm_changed_id; +}; + +G_DEFINE_TYPE (CcKeyboardManager, cc_keyboard_manager, G_TYPE_OBJECT) + +enum +{ + SHORTCUT_ADDED, + SHORTCUT_CHANGED, + SHORTCUT_REMOVED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +/* + * Auxiliary methos + */ +static void +free_key_array (GPtrArray *keys) +{ + if (keys != NULL) + { + gint i; + + for (i = 0; i < keys->len; i++) + { + CcKeyboardItem *item; + + item = g_ptr_array_index (keys, i); + + g_object_unref (item); + } + + g_ptr_array_free (keys, TRUE); + } +} + +static gboolean +find_conflict (CcUniquenessData *data, + CcKeyboardItem *item) +{ + GList *l; + gboolean is_conflict = FALSE; + + if (data->orig_item && cc_keyboard_item_equal (data->orig_item, item)) + return FALSE; + + for (l = cc_keyboard_item_get_key_combos (item); l; l = l->next) + { + CcKeyCombo *combo = l->data; + + if (data->new_mask != combo->mask) + continue; + + if (data->new_keyval != 0) + is_conflict = data->new_keyval == combo->keyval; + else + is_conflict = combo->keyval == 0 && data->new_keycode == combo->keycode; + + if (is_conflict) + break; + } + + if (is_conflict) + data->conflict_item = item; + + return is_conflict; +} + +static gboolean +compare_keys_for_uniqueness (CcKeyboardItem *current_item, + CcUniquenessData *data) +{ + CcKeyboardItem *reverse_item; + + /* No conflict for: blanks or ourselves */ + if (!current_item || data->orig_item == current_item) + return FALSE; + + reverse_item = cc_keyboard_item_get_reverse_item (current_item); + + /* When the current item is the reversed shortcut of a main item, simply ignore it */ + if (reverse_item && cc_keyboard_item_is_hidden (current_item)) + return FALSE; + + if (find_conflict (data, current_item)) + return TRUE; + + /* Also check for the reverse item if any */ + if (reverse_item && find_conflict (data, reverse_item)) + return TRUE; + + return FALSE; +} + +static gboolean +check_for_uniqueness (gpointer key, + GPtrArray *keys_array, + CcUniquenessData *data) +{ + guint i; + + for (i = 0; i < keys_array->len; i++) + { + CcKeyboardItem *item; + + item = keys_array->pdata[i]; + + if (compare_keys_for_uniqueness (item, data)) + return TRUE; + } + + return FALSE; +} + + +static GHashTable* +get_hash_for_group (CcKeyboardManager *self, + BindingGroupType group) +{ + GHashTable *hash; + + switch (group) + { + case BINDING_GROUP_SYSTEM: + hash = self->kb_system_sections; + break; + case BINDING_GROUP_APPS: + hash = self->kb_apps_sections; + break; + case BINDING_GROUP_USER: + hash = self->kb_user_sections; + break; + default: + hash = NULL; + } + + return hash; +} + +static gboolean +have_key_for_group (CcKeyboardManager *self, + int group, + const gchar *name) +{ + GHashTableIter iter; + GPtrArray *keys; + gint i; + + g_hash_table_iter_init (&iter, get_hash_for_group (self, group)); + while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &keys)) + { + for (i = 0; i < keys->len; i++) + { + CcKeyboardItem *item = g_ptr_array_index (keys, i); + + if (cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS && + g_strcmp0 (name, cc_keyboard_item_get_key (item)) == 0) + { + return TRUE; + } + } + } + + return FALSE; +} + +static void +add_shortcuts (CcKeyboardManager *self) +{ + GtkTreeModel *sections_model; + GtkTreeIter sections_iter; + gboolean can_continue; + + sections_model = GTK_TREE_MODEL (self->sections_store); + can_continue = gtk_tree_model_get_iter_first (sections_model, §ions_iter); + + while (can_continue) + { + BindingGroupType group; + GPtrArray *keys; + g_autofree gchar *id = NULL; + g_autofree gchar *title = NULL; + gint i; + + gtk_tree_model_get (sections_model, + §ions_iter, + SECTION_DESCRIPTION_COLUMN, &title, + SECTION_GROUP_COLUMN, &group, + SECTION_ID_COLUMN, &id, + -1); + + /* Ignore separators */ + if (group == BINDING_GROUP_SEPARATOR) + { + can_continue = gtk_tree_model_iter_next (sections_model, §ions_iter); + continue; + } + + keys = g_hash_table_lookup (get_hash_for_group (self, group), id); + + for (i = 0; i < keys->len; i++) + { + CcKeyboardItem *item = g_ptr_array_index (keys, i); + + if (!cc_keyboard_item_is_hidden (item)) + { + g_signal_emit (self, signals[SHORTCUT_ADDED], + 0, + item, + id, + title); + } + } + + can_continue = gtk_tree_model_iter_next (sections_model, §ions_iter); + } +} + +static void +append_section (CcKeyboardManager *self, + const gchar *title, + const gchar *id, + BindingGroupType group, + const KeyListEntry *keys_list) +{ + GtkTreeIter iter; + GHashTable *reverse_items; + GHashTable *hash; + GPtrArray *keys_array; + gboolean is_new; + gint i; + + hash = get_hash_for_group (self, group); + + if (!hash) + return; + + /* Add all CcKeyboardItems for this section */ + is_new = FALSE; + keys_array = g_hash_table_lookup (hash, id); + if (keys_array == NULL) + { + keys_array = g_ptr_array_new (); + is_new = TRUE; + } + + reverse_items = g_hash_table_new (g_str_hash, g_str_equal); + + for (i = 0; keys_list != NULL && keys_list[i].name != NULL; i++) + { + CcKeyboardItem *item; + gboolean ret; + + if (have_key_for_group (self, group, keys_list[i].name)) + continue; + + item = cc_keyboard_item_new (keys_list[i].type); + + switch (keys_list[i].type) + { + case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH: + ret = cc_keyboard_item_load_from_gsettings_path (item, keys_list[i].name, FALSE); + break; + + case CC_KEYBOARD_ITEM_TYPE_GSETTINGS: + ret = cc_keyboard_item_load_from_gsettings (item, + keys_list[i].description, + keys_list[i].schema, + keys_list[i].name); + if (ret && keys_list[i].reverse_entry != NULL) + { + CcKeyboardItem *reverse_item; + reverse_item = g_hash_table_lookup (reverse_items, + keys_list[i].reverse_entry); + if (reverse_item != NULL) + { + cc_keyboard_item_add_reverse_item (item, + reverse_item, + keys_list[i].is_reversed); + } + else + { + g_hash_table_insert (reverse_items, + keys_list[i].name, + item); + } + } + break; + + default: + g_assert_not_reached (); + } + + if (ret == FALSE) + { + /* We don't actually want to popup a dialog - just skip this one */ + g_object_unref (item); + continue; + } + + cc_keyboard_item_set_hidden (item, keys_list[i].hidden); + + g_ptr_array_add (keys_array, item); + } + + g_hash_table_destroy (reverse_items); + + /* Add the keys to the hash table */ + if (is_new) + { + g_hash_table_insert (hash, g_strdup (id), keys_array); + + /* Append the section to the left tree view */ + gtk_list_store_append (GTK_LIST_STORE (self->sections_store), &iter); + gtk_list_store_set (GTK_LIST_STORE (self->sections_store), + &iter, + SECTION_DESCRIPTION_COLUMN, title, + SECTION_ID_COLUMN, id, + SECTION_GROUP_COLUMN, group, + -1); + } +} + +static void +append_sections_from_file (CcKeyboardManager *self, + const gchar *path, + const char *datadir, + gchar **wm_keybindings) +{ + KeyList *keylist; + KeyListEntry *keys; + KeyListEntry key = { 0, 0, 0, 0, 0, 0, 0 }; + const char *title; + int group; + guint i; + + keylist = parse_keylist_from_file (path); + + if (keylist == NULL) + return; + +#define const_strv(s) ((const gchar* const*) s) + + /* If there's no keys to add, or the settings apply to a window manager + * that's not the one we're running */ + if (keylist->entries->len == 0 || + (keylist->wm_name != NULL && !g_strv_contains (const_strv (wm_keybindings), keylist->wm_name)) || + keylist->name == NULL) + { + g_free (keylist->name); + g_free (keylist->package); + g_free (keylist->wm_name); + g_array_free (keylist->entries, TRUE); + g_free (keylist); + return; + } + +#undef const_strv + + /* Empty KeyListEntry to end the array */ + key.name = NULL; + g_array_append_val (keylist->entries, key); + + keys = (KeyListEntry *) g_array_free (keylist->entries, FALSE); + if (keylist->package) + { + g_autofree gchar *localedir = NULL; + + localedir = g_build_filename (datadir, "locale", NULL); + bindtextdomain (keylist->package, localedir); + + title = dgettext (keylist->package, keylist->name); + } else { + title = _(keylist->name); + } + + if (keylist->group && strcmp (keylist->group, "system") == 0) + group = BINDING_GROUP_SYSTEM; + else + group = BINDING_GROUP_APPS; + + append_section (self, title, keylist->name, group, keys); + + g_free (keylist->name); + g_free (keylist->package); + g_free (keylist->wm_name); + g_free (keylist->schema); + g_free (keylist->group); + + for (i = 0; keys[i].name != NULL; i++) + { + KeyListEntry *entry = &keys[i]; + g_free (entry->schema); + g_free (entry->description); + g_free (entry->name); + g_free (entry->reverse_entry); + } + + g_free (keylist); + g_free (keys); +} + +static void +append_sections_from_gsettings (CcKeyboardManager *self) +{ + g_auto(GStrv) custom_paths = NULL; + GArray *entries; + KeyListEntry key = { 0, 0, 0, 0, 0, 0, 0 }; + int i; + + /* load custom shortcuts from GSettings */ + entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry)); + + custom_paths = g_settings_get_strv (self->binding_settings, "custom-keybindings"); + for (i = 0; custom_paths[i]; i++) + { + key.name = g_strdup (custom_paths[i]); + if (!have_key_for_group (self, BINDING_GROUP_USER, key.name)) + { + key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH; + g_array_append_val (entries, key); + } + else + g_free (key.name); + } + + if (entries->len > 0) + { + KeyListEntry *keys; + int i; + + /* Empty KeyListEntry to end the array */ + key.name = NULL; + g_array_append_val (entries, key); + + keys = (KeyListEntry *) entries->data; + append_section (self, _("Custom Shortcuts"), CUSTOM_SHORTCUTS_ID, BINDING_GROUP_USER, keys); + for (i = 0; i < entries->len; ++i) + { + g_free (keys[i].name); + } + } + else + { + append_section (self, _("Custom Shortcuts"), CUSTOM_SHORTCUTS_ID, BINDING_GROUP_USER, NULL); + } + + g_array_free (entries, TRUE); +} + +static void +reload_sections (CcKeyboardManager *self) +{ + GHashTable *loaded_files; + GDir *dir; + gchar *default_wm_keybindings[] = { "Mutter", "GNOME Shell", NULL }; + g_auto(GStrv) wm_keybindings = NULL; + const gchar * const * data_dirs; + guint i; + + /* Clear previous models and hash tables */ + gtk_list_store_clear (GTK_LIST_STORE (self->sections_store)); + + g_clear_pointer (&self->kb_system_sections, g_hash_table_destroy); + self->kb_system_sections = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) free_key_array); + + g_clear_pointer (&self->kb_apps_sections, g_hash_table_destroy); + self->kb_apps_sections = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) free_key_array); + + g_clear_pointer (&self->kb_user_sections, g_hash_table_destroy); + self->kb_user_sections = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) free_key_array); + + /* Load WM keybindings */ +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + wm_keybindings = wm_common_get_current_keybindings (); + else +#endif + wm_keybindings = g_strdupv (default_wm_keybindings); + + loaded_files = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + data_dirs = g_get_system_data_dirs (); + for (i = 0; data_dirs[i] != NULL; i++) + { + g_autofree gchar *dir_path = NULL; + const gchar *name; + + dir_path = g_build_filename (data_dirs[i], "gnome-control-center", "keybindings", NULL); + + dir = g_dir_open (dir_path, 0, NULL); + if (!dir) + continue; + + for (name = g_dir_read_name (dir) ; name ; name = g_dir_read_name (dir)) + { + g_autofree gchar *path = NULL; + + if (g_str_has_suffix (name, ".xml") == FALSE) + continue; + + if (g_hash_table_lookup (loaded_files, name) != NULL) + { + g_debug ("Not loading %s, it was already loaded from another directory", name); + continue; + } + + g_hash_table_insert (loaded_files, g_strdup (name), GINT_TO_POINTER (1)); + path = g_build_filename (dir_path, name, NULL); + append_sections_from_file (self, path, data_dirs[i], wm_keybindings); + } + + g_dir_close (dir); + } + + g_hash_table_destroy (loaded_files); + + /* Load custom keybindings */ + append_sections_from_gsettings (self); +} + +/* + * Callbacks + */ +static void +on_window_manager_change (const char *wm_name, + CcKeyboardManager *self) +{ + reload_sections (self); +} + +static void +cc_keyboard_manager_finalize (GObject *object) +{ + CcKeyboardManager *self = (CcKeyboardManager *)object; + + g_clear_pointer (&self->kb_system_sections, g_hash_table_destroy); + g_clear_pointer (&self->kb_apps_sections, g_hash_table_destroy); + g_clear_pointer (&self->kb_user_sections, g_hash_table_destroy); + g_clear_object (&self->binding_settings); + + g_clear_pointer (&self->wm_changed_id, wm_common_unregister_window_manager_change); + + G_OBJECT_CLASS (cc_keyboard_manager_parent_class)->finalize (object); +} + +static void +cc_keyboard_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +cc_keyboard_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); +} + +static void +cc_keyboard_manager_class_init (CcKeyboardManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cc_keyboard_manager_finalize; + object_class->get_property = cc_keyboard_manager_get_property; + object_class->set_property = cc_keyboard_manager_set_property; + + /** + * CcKeyboardManager:shortcut-added: + * + * Emitted when a shortcut is added. + */ + signals[SHORTCUT_ADDED] = g_signal_new ("shortcut-added", + CC_TYPE_KEYBOARD_MANAGER, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 3, + CC_TYPE_KEYBOARD_ITEM, + G_TYPE_STRING, + G_TYPE_STRING); + + /** + * CcKeyboardManager:shortcut-changed: + * + * Emitted when a shortcut is added. + */ + signals[SHORTCUT_CHANGED] = g_signal_new ("shortcut-changed", + CC_TYPE_KEYBOARD_MANAGER, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + CC_TYPE_KEYBOARD_ITEM); + + + /** + * CcKeyboardManager:shortcut-removed: + * + * Emitted when a shortcut is removed. + */ + signals[SHORTCUT_REMOVED] = g_signal_new ("shortcut-removed", + CC_TYPE_KEYBOARD_MANAGER, + G_SIGNAL_RUN_FIRST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + CC_TYPE_KEYBOARD_ITEM); +} + +static void +cc_keyboard_manager_init (CcKeyboardManager *self) +{ + /* Bindings */ + self->binding_settings = g_settings_new (BINDINGS_SCHEMA); + + /* Setup the section models */ + self->sections_store = gtk_list_store_new (SECTION_N_COLUMNS, + G_TYPE_STRING, + G_TYPE_STRING, + G_TYPE_INT); + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (gdk_display_get_default ())) + self->wm_changed_id = wm_common_register_window_manager_change ((GFunc) on_window_manager_change, + self); +#endif +} + + +CcKeyboardManager * +cc_keyboard_manager_new (void) +{ + return g_object_new (CC_TYPE_KEYBOARD_MANAGER, NULL); +} + +void +cc_keyboard_manager_load_shortcuts (CcKeyboardManager *self) +{ + g_return_if_fail (CC_IS_KEYBOARD_MANAGER (self)); + + reload_sections (self); + add_shortcuts (self); +} + +/** + * cc_keyboard_manager_create_custom_shortcut: + * @self: a #CcKeyboardPanel + * + * Creates a new temporary keyboard shortcut. + * + * Returns: (transfer full): a #CcKeyboardItem + */ +CcKeyboardItem* +cc_keyboard_manager_create_custom_shortcut (CcKeyboardManager *self) +{ + CcKeyboardItem *item; + g_autofree gchar *settings_path = NULL; + + g_return_val_if_fail (CC_IS_KEYBOARD_MANAGER (self), NULL); + + item = cc_keyboard_item_new (CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH); + + settings_path = find_free_settings_path (self->binding_settings); + cc_keyboard_item_load_from_gsettings_path (item, settings_path, TRUE); + + return item; +} + +/** + * cc_keyboard_manager_add_custom_shortcut: + * @self: a #CcKeyboardPanel + * @item: the #CcKeyboardItem to be added + * + * Effectively adds the custom shortcut. + */ +void +cc_keyboard_manager_add_custom_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item) +{ + GPtrArray *keys_array; + GHashTable *hash; + GVariantBuilder builder; + char **settings_paths; + int i; + + g_return_if_fail (CC_IS_KEYBOARD_MANAGER (self)); + + hash = get_hash_for_group (self, BINDING_GROUP_USER); + keys_array = g_hash_table_lookup (hash, CUSTOM_SHORTCUTS_ID); + + if (keys_array == NULL) + { + keys_array = g_ptr_array_new (); + g_hash_table_insert (hash, g_strdup (CUSTOM_SHORTCUTS_ID), keys_array); + } + + g_ptr_array_add (keys_array, item); + + settings_paths = g_settings_get_strv (self->binding_settings, "custom-keybindings"); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + + for (i = 0; settings_paths[i]; i++) + g_variant_builder_add (&builder, "s", settings_paths[i]); + + g_variant_builder_add (&builder, "s", cc_keyboard_item_get_gsettings_path (item)); + + g_settings_set_value (self->binding_settings, "custom-keybindings", g_variant_builder_end (&builder)); + + g_signal_emit (self, signals[SHORTCUT_ADDED], + 0, + item, + CUSTOM_SHORTCUTS_ID, + _("Custom Shortcuts")); +} + +/** + * cc_keyboard_manager_remove_custom_shortcut: + * @self: a #CcKeyboardPanel + * @item: the #CcKeyboardItem to be added + * + * Removed the custom shortcut. + */ +void +cc_keyboard_manager_remove_custom_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item) +{ + GPtrArray *keys_array; + GVariantBuilder builder; + GSettings *settings; + char **settings_paths; + int i; + + g_return_if_fail (CC_IS_KEYBOARD_MANAGER (self)); + + /* Shortcut not a custom shortcut */ + g_assert (cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH); + + settings = cc_keyboard_item_get_settings (item); + g_settings_delay (settings); + g_settings_reset (settings, "name"); + g_settings_reset (settings, "command"); + g_settings_reset (settings, "binding"); + g_settings_apply (settings); + g_settings_sync (); + + settings_paths = g_settings_get_strv (self->binding_settings, "custom-keybindings"); + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + + for (i = 0; settings_paths[i]; i++) + if (strcmp (settings_paths[i], cc_keyboard_item_get_gsettings_path (item)) != 0) + g_variant_builder_add (&builder, "s", settings_paths[i]); + + g_settings_set_value (self->binding_settings, + "custom-keybindings", + g_variant_builder_end (&builder)); + + g_strfreev (settings_paths); + + keys_array = g_hash_table_lookup (get_hash_for_group (self, BINDING_GROUP_USER), CUSTOM_SHORTCUTS_ID); + g_ptr_array_remove (keys_array, item); + + g_signal_emit (self, signals[SHORTCUT_REMOVED], 0, item); +} + +/** + * cc_keyboard_manager_get_collision: + * @self: a #CcKeyboardManager + * @item: (nullable): a keyboard shortcut + * @combo: a #CcKeyCombo + * + * Retrieves the collision item for the given shortcut. + * + * Returns: (transfer none)(nullable): the collisioned shortcut + */ +CcKeyboardItem* +cc_keyboard_manager_get_collision (CcKeyboardManager *self, + CcKeyboardItem *item, + CcKeyCombo *combo) +{ + CcUniquenessData data; + BindingGroupType i; + + g_return_val_if_fail (CC_IS_KEYBOARD_MANAGER (self), NULL); + + data.orig_item = item; + data.new_keyval = combo->keyval; + data.new_mask = combo->mask; + data.new_keycode = combo->keycode; + data.conflict_item = NULL; + + if (combo->keyval == 0 && combo->keycode == 0) + return NULL; + + /* Any number of shortcuts can be disabled */ + for (i = BINDING_GROUP_SYSTEM; i <= BINDING_GROUP_USER && !data.conflict_item; i++) + { + GHashTable *table; + + table = get_hash_for_group (self, i); + + if (!table) + continue; + + g_hash_table_find (table, (GHRFunc) check_for_uniqueness, &data); + } + + return data.conflict_item; +} + +/** + * cc_keyboard_manager_disable_shortcut: + * @self: a #CcKeyboardManager + * @item: a @CcKeyboardItem + * + * Disables the given keyboard shortcut. + */ +void +cc_keyboard_manager_disable_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item) +{ + g_return_if_fail (CC_IS_KEYBOARD_MANAGER (self)); + + g_object_set (item, "binding", NULL, NULL); +} + +/** + * cc_keyboard_manager_reset_shortcut: + * @self: a #CcKeyboardManager + * @item: a #CcKeyboardItem + * + * Resets the keyboard shortcut managed by @item, and eventually + * disables any shortcut that conflicts with the new shortcut's + * value. + */ +void +cc_keyboard_manager_reset_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item) +{ + GList *l; + + g_return_if_fail (CC_IS_KEYBOARD_MANAGER (self)); + g_return_if_fail (CC_IS_KEYBOARD_ITEM (item)); + + /* Disables any shortcut that conflicts with the new shortcut's value */ + for (l = cc_keyboard_item_get_default_combos (item); l; l = l->next) + { + CcKeyCombo *combo = l->data; + CcKeyboardItem *collision; + + collision = cc_keyboard_manager_get_collision (self, NULL, combo); + if (collision) + cc_keyboard_manager_disable_shortcut (self, collision); + } + + /* Resets the current item */ + cc_keyboard_item_reset (item); +} diff --git a/panels/keyboard/cc-keyboard-manager.h b/panels/keyboard/cc-keyboard-manager.h new file mode 100644 index 0000000..2b03752 --- /dev/null +++ b/panels/keyboard/cc-keyboard-manager.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 Intel, Inc + * Copyright (C) 2016 Endless, 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 <http://www.gnu.org/licenses/>. + * + * Author: Thomas Wood <thomas.wood@intel.com> + * Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + * + */ + +#pragma once + +#include <glib-object.h> + +#include "cc-keyboard-item.h" + +G_BEGIN_DECLS + +#define CC_TYPE_KEYBOARD_MANAGER (cc_keyboard_manager_get_type ()) +G_DECLARE_FINAL_TYPE (CcKeyboardManager, cc_keyboard_manager, CC, KEYBOARD_MANAGER, GObject) + +CcKeyboardManager* cc_keyboard_manager_new (void); + +void cc_keyboard_manager_load_shortcuts (CcKeyboardManager *self); + +CcKeyboardItem* cc_keyboard_manager_create_custom_shortcut (CcKeyboardManager *self); + +void cc_keyboard_manager_add_custom_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item); + +void cc_keyboard_manager_remove_custom_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item); + +CcKeyboardItem* cc_keyboard_manager_get_collision (CcKeyboardManager *self, + CcKeyboardItem *item, + CcKeyCombo *combo); + +void cc_keyboard_manager_disable_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item); + +void cc_keyboard_manager_reset_shortcut (CcKeyboardManager *self, + CcKeyboardItem *item); + +G_END_DECLS + diff --git a/panels/keyboard/cc-keyboard-option.c b/panels/keyboard/cc-keyboard-option.c new file mode 100644 index 0000000..5730fdc --- /dev/null +++ b/panels/keyboard/cc-keyboard-option.c @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2012 Red Hat, Inc. + * + * Written by: Rui Matos <rmatos@redhat.com> + * + * 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, 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include <config.h> +#include <glib/gi18n.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-xkb-info.h> + +#include "cc-keyboard-option.h" + +#define INPUT_SOURCES_SCHEMA "org.gnome.desktop.input-sources" +#define XKB_OPTIONS_KEY "xkb-options" + +#define XKB_OPTION_GROUP_LVL3 "lv3" +#define XKB_OPTION_GROUP_COMP "Compose key" +#define XKB_OPTION_GROUP_GRP "grp" + +enum +{ + PROP_0, + PROP_GROUP, + PROP_DESCRIPTION +}; + +enum +{ + CHANGED_SIGNAL, + LAST_SIGNAL +}; + +struct _CcKeyboardOption +{ + gchar *group; + gchar *description; + gchar *current_value; + GtkListStore *store; + + const gchar * const *allowed_xkb_options; +}; + +G_DEFINE_TYPE (CcKeyboardOption, cc_keyboard_option, G_TYPE_OBJECT); + +static guint keyboard_option_signals[LAST_SIGNAL] = { 0 }; + +static GnomeXkbInfo *xkb_info = NULL; +static GSettings *input_sources_settings = NULL; +static gchar **current_xkb_options = NULL; + +static const gchar *allowed_xkb_lvl3_options[] = { + "lv3:switch", + "lv3:menu_switch", + "lv3:rwin_switch", + "lv3:lalt_switch", + "lv3:ralt_switch", + "lv3:caps_switch", + "lv3:enter_switch", + "lv3:bksl_switch", + "lv3:lsgt_switch", + NULL +}; + +static const gchar *allowedd_xkb_compose_options[] = { + "compose:ralt", + "compose:rwin", + "compose:menu", + "compose:lctrl", + "compose:rctrl", + "compose:caps", + "compose:prsc", + "compose:sclk", + NULL +}; + +/* This list must be kept in sync with what mutter is able to + * handle. */ +static const gchar *allowed_xkb_grp_options[] = { + "grp:toggle", + "grp:lalt_toggle", + "grp:lwin_toggle", + "grp:rwin_toggle", + "grp:lshift_toggle", + "grp:rshift_toggle", + "grp:lctrl_toggle", + "grp:rctrl_toggle", + "grp:sclk_toggle", + "grp:menu_toggle", + "grp:caps_toggle", + "grp:shift_caps_toggle", + "grp:alt_caps_toggle", + "grp:alt_space_toggle", + "grp:ctrl_shift_toggle", + "grp:lctrl_lshift_toggle", + "grp:rctrl_rshift_toggle", + "grp:ctrl_alt_toggle", + "grp:alt_shift_toggle", + "grp:lalt_lshift_toggle", + NULL +}; + +static GList *objects_list = NULL; + +static gboolean +strv_contains (const gchar * const *strv, + const gchar *str) +{ + const gchar * const *p = strv; + for (p = strv; *p; p++) + if (g_strcmp0 (*p, str) == 0) + return TRUE; + + return FALSE; +} + +static void +reload_setting (CcKeyboardOption *self) +{ + gchar **iter; + + for (iter = current_xkb_options; *iter; ++iter) + if (strv_contains (self->allowed_xkb_options, *iter)) + { + if (g_strcmp0 (self->current_value, *iter) != 0) + { + g_free (self->current_value); + self->current_value = g_strdup (*iter); + g_signal_emit (self, keyboard_option_signals[CHANGED_SIGNAL], 0); + } + break; + } + + if (*iter == NULL && self->current_value != NULL) + { + g_clear_pointer (&self->current_value, g_free); + g_signal_emit (self, keyboard_option_signals[CHANGED_SIGNAL], 0); + } +} + +static void +xkb_options_changed (GSettings *settings, + gchar *key, + gpointer data) +{ + GList *l; + + g_strfreev (current_xkb_options); + current_xkb_options = g_settings_get_strv (settings, key); + + for (l = objects_list; l; l = l->next) + reload_setting (CC_KEYBOARD_OPTION (l->data)); +} + +static void +cc_keyboard_option_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcKeyboardOption *self; + + self = CC_KEYBOARD_OPTION (object); + + switch (prop_id) + { + case PROP_GROUP: + g_value_set_string (value, self->group); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->description); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cc_keyboard_option_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcKeyboardOption *self; + + self = CC_KEYBOARD_OPTION (object); + + switch (prop_id) + { + case PROP_GROUP: + self->group = g_value_dup_string (value); + break; + case PROP_DESCRIPTION: + self->description = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cc_keyboard_option_init (CcKeyboardOption *self) +{ +} + +static void +cc_keyboard_option_finalize (GObject *object) +{ + CcKeyboardOption *self = CC_KEYBOARD_OPTION (object); + + g_clear_pointer (&self->group, g_free); + g_clear_pointer (&self->description, g_free); + g_clear_pointer (&self->current_value, g_free); + g_clear_object (&self->store); + + G_OBJECT_CLASS (cc_keyboard_option_parent_class)->finalize (object); +} + +static void +cc_keyboard_option_constructed (GObject *object) +{ + GtkTreeIter iter; + GList *options, *l; + gchar *option_id; + CcKeyboardOption *self = CC_KEYBOARD_OPTION (object); + + G_OBJECT_CLASS (cc_keyboard_option_parent_class)->constructed (object); + + if (g_str_equal (self->group, XKB_OPTION_GROUP_LVL3)) + self->allowed_xkb_options = allowed_xkb_lvl3_options; + else if (g_str_equal (self->group, XKB_OPTION_GROUP_COMP)) + self->allowed_xkb_options = allowedd_xkb_compose_options; + else if (g_str_equal (self->group, XKB_OPTION_GROUP_GRP)) + self->allowed_xkb_options = allowed_xkb_grp_options; + else + g_assert_not_reached (); + + self->store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING); + gtk_list_store_append (self->store, &iter); + gtk_list_store_set (self->store, &iter, + XKB_OPTION_DESCRIPTION_COLUMN, _("Disabled"), + XKB_OPTION_ID_COLUMN, NULL, + -1); + options = gnome_xkb_info_get_options_for_group (xkb_info, self->group); + for (l = options; l; l = l->next) + { + option_id = l->data; + if (strv_contains (self->allowed_xkb_options, option_id)) + { + gtk_list_store_append (self->store, &iter); + gtk_list_store_set (self->store, &iter, + XKB_OPTION_DESCRIPTION_COLUMN, + gnome_xkb_info_description_for_option (xkb_info, self->group, option_id), + XKB_OPTION_ID_COLUMN, + option_id, + -1); + } + } + g_list_free (options); + + reload_setting (self); +} + +static void +cc_keyboard_option_class_init (CcKeyboardOptionClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = cc_keyboard_option_get_property; + gobject_class->set_property = cc_keyboard_option_set_property; + gobject_class->finalize = cc_keyboard_option_finalize; + gobject_class->constructed = cc_keyboard_option_constructed; + + g_object_class_install_property (gobject_class, + PROP_GROUP, + g_param_spec_string ("group", + "group", + "xkb option group identifier", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); + g_object_class_install_property (gobject_class, + PROP_DESCRIPTION, + g_param_spec_string ("description", + "description", + "translated option description", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE)); + + keyboard_option_signals[CHANGED_SIGNAL] = g_signal_new ("changed", + CC_TYPE_KEYBOARD_OPTION, + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + +GList * +cc_keyboard_option_get_all (void) +{ + if (objects_list) + return objects_list; + + xkb_info = gnome_xkb_info_new (); + + input_sources_settings = g_settings_new (INPUT_SOURCES_SCHEMA); + + g_signal_connect (input_sources_settings, "changed::" XKB_OPTIONS_KEY, + G_CALLBACK (xkb_options_changed), NULL); + + xkb_options_changed (input_sources_settings, XKB_OPTIONS_KEY, NULL); + + objects_list = g_list_prepend (objects_list, + g_object_new (CC_TYPE_KEYBOARD_OPTION, + "group", XKB_OPTION_GROUP_LVL3, + /* Translators: This key is also known as 'third level + * chooser'. AltGr is often used for this purpose. See + * https://live.gnome.org/Design/SystemSettings/RegionAndLanguage + */ + "description", _("Alternative Characters Key"), + NULL)); + objects_list = g_list_prepend (objects_list, + g_object_new (CC_TYPE_KEYBOARD_OPTION, + "group", XKB_OPTION_GROUP_COMP, + /* Translators: The Compose key is used to initiate key + * sequences that are combined to form a single character. + * See http://en.wikipedia.org/wiki/Compose_key + */ + "description", _("Compose Key"), + NULL)); + objects_list = g_list_prepend (objects_list, + g_object_new (CC_TYPE_KEYBOARD_OPTION, + "group", XKB_OPTION_GROUP_GRP, + "description", _("Modifiers-only switch to next source"), + NULL)); + return objects_list; +} + +const gchar * +cc_keyboard_option_get_description (CcKeyboardOption *self) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_OPTION (self), NULL); + + return self->description; +} + +GtkListStore * +cc_keyboard_option_get_store (CcKeyboardOption *self) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_OPTION (self), NULL); + + return self->store; +} + +const gchar * +cc_keyboard_option_get_current_value_description (CcKeyboardOption *self) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_OPTION (self), NULL); + + if (!self->current_value) + return _("Disabled"); + + return gnome_xkb_info_description_for_option (xkb_info, self->group, self->current_value); +} + +static void +remove_value (const gchar *value) +{ + gchar **p; + + for (p = current_xkb_options; *p; ++p) + if (g_str_equal (*p, value)) + { + g_free (*p); + break; + } + + for (++p; *p; ++p) + *(p - 1) = *p; + + *(p - 1) = NULL; +} + +static void +add_value (const gchar *value) +{ + gchar **new_xkb_options; + gchar **a, **b; + + new_xkb_options = g_new0 (gchar *, g_strv_length (current_xkb_options) + 2); + + a = new_xkb_options; + for (b = current_xkb_options; *b; ++a, ++b) + *a = g_strdup (*b); + + *a = g_strdup (value); + + g_strfreev (current_xkb_options); + current_xkb_options = new_xkb_options; +} + +static void +replace_value (const gchar *old, + const gchar *new) +{ + gchar **iter; + + if (g_str_equal (old, new)) + return; + + for (iter = current_xkb_options; *iter; ++iter) + if (g_str_equal (*iter, old)) + { + g_free (*iter); + *iter = g_strdup (new); + break; + } +} + +void +cc_keyboard_option_set_selection (CcKeyboardOption *self, + GtkTreeIter *iter) +{ + g_autofree gchar *new_value = NULL; + + g_return_if_fail (CC_IS_KEYBOARD_OPTION (self)); + + gtk_tree_model_get (GTK_TREE_MODEL (self->store), iter, + XKB_OPTION_ID_COLUMN, &new_value, + -1); + + if (!new_value) + { + if (self->current_value) + remove_value (self->current_value); + } + else + { + if (self->current_value) + replace_value (self->current_value, new_value); + else + add_value (new_value); + } + + g_settings_set_strv (input_sources_settings, XKB_OPTIONS_KEY, + (const gchar * const *) current_xkb_options); +} + +void +cc_keyboard_option_clear_all (void) +{ + GList *l; + + for (l = objects_list; l; l = l->next) + g_object_unref (l->data); + + g_clear_pointer (&objects_list, g_list_free); + g_clear_pointer (¤t_xkb_options, g_strfreev); + g_clear_object (&input_sources_settings); + g_clear_object (&xkb_info); +} diff --git a/panels/keyboard/cc-keyboard-option.h b/panels/keyboard/cc-keyboard-option.h new file mode 100644 index 0000000..8bd0cd6 --- /dev/null +++ b/panels/keyboard/cc-keyboard-option.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 Red Hat, Inc. + * + * Written by: Rui Matos <rmatos@redhat.com> + * + * 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, 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., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#pragma once + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_KEYBOARD_OPTION (cc_keyboard_option_get_type ()) +G_DECLARE_FINAL_TYPE (CcKeyboardOption, cc_keyboard_option, CC, KEYBOARD_OPTION, GObject) + +enum +{ + XKB_OPTION_DESCRIPTION_COLUMN, + XKB_OPTION_ID_COLUMN, + XKB_OPTION_N_COLUMNS +}; + +GList * cc_keyboard_option_get_all (void); +const gchar * cc_keyboard_option_get_description (CcKeyboardOption *self); +GtkListStore * cc_keyboard_option_get_store (CcKeyboardOption *self); +const gchar * cc_keyboard_option_get_current_value_description (CcKeyboardOption *self); +void cc_keyboard_option_set_selection (CcKeyboardOption *self, + GtkTreeIter *iter); +void cc_keyboard_option_clear_all (void); + +G_END_DECLS diff --git a/panels/keyboard/cc-keyboard-panel.c b/panels/keyboard/cc-keyboard-panel.c new file mode 100644 index 0000000..f47e34c --- /dev/null +++ b/panels/keyboard/cc-keyboard-panel.c @@ -0,0 +1,844 @@ +/* + * Copyright (C) 2010 Intel, Inc + * Copyright (C) 2016 Endless, 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 <http://www.gnu.org/licenses/>. + * + * Author: Thomas Wood <thomas.wood@intel.com> + * Georges Basile Stavracas Neto <gbsneto@gnome.org> + * + */ + +#include <glib/gi18n.h> + +#include "cc-alt-chars-key-dialog.h" +#include "cc-keyboard-item.h" +#include "cc-keyboard-manager.h" +#include "cc-keyboard-option.h" +#include "cc-keyboard-panel.h" +#include "cc-keyboard-resources.h" +#include "cc-keyboard-shortcut-editor.h" + +#include "keyboard-shortcuts.h" + +#include "cc-util.h" + +#define SHORTCUT_DELIMITERS "+ " + +typedef struct { + CcKeyboardItem *item; + gchar *section_title; + gchar *section_id; +} RowData; + +struct _CcKeyboardPanel +{ + CcPanel parent_instance; + + /* Search */ + GtkWidget *empty_search_placeholder; + GtkWidget *reset_button; + GtkWidget *search_bar; + GtkWidget *search_button; + GtkWidget *search_entry; + guint search_bar_handler_id; + + /* Shortcuts */ + GtkWidget *shortcuts_listbox; + GtkListBoxRow *add_shortcut_row; + GtkSizeGroup *accelerator_sizegroup; + + /* Alternate characters key */ + CcAltCharsKeyDialog *alt_chars_key_dialog; + GSettings *input_source_settings; + GtkWidget *value_alternate_chars; + + /* Custom shortcut dialog */ + GtkWidget *shortcut_editor; + + GRegex *pictures_regex; + + CcKeyboardManager *manager; +}; + +CC_PANEL_REGISTER (CcKeyboardPanel, cc_keyboard_panel) + +enum { + PROP_0, + PROP_PARAMETERS +}; + +static const gchar* custom_css = +"button.reset-shortcut-button {" +" padding: 0;" +"}"; + + +#define DEFAULT_LV3_OPTION 5 +static struct { + const char *xkb_option; + const char *label; + const char *widget_name; +} lv3_xkb_options[] = { + { "lv3:switch", NC_("keyboard key", "Right Ctrl"), "radiobutton_rightctrl" }, + { "lv3:menu_switch", NC_("keyboard key", "Menu Key"), "radiobutton_menukey" }, + { "lv3:lwin_switch", NC_("keyboard key", "Left Super"), "radiobutton_leftsuper" }, + { "lv3:rwin_switch", NC_("keyboard key", "Right Super"), "radiobutton_rightsuper" }, + { "lv3:lalt_switch", NC_("keyboard key", "Left Alt"), "radiobutton_leftalt" }, + { "lv3:ralt_switch", NC_("keyboard key", "Right Alt"), "radiobutton_rightalt" }, +}; + +/* RowData functions */ +static RowData * +row_data_new (CcKeyboardItem *item, + const gchar *section_id, + const gchar *section_title) +{ + RowData *data; + + data = g_new0 (RowData, 1); + data->item = g_object_ref (item); + data->section_id = g_strdup (section_id); + data->section_title = g_strdup (section_title); + + return data; +} + +static void +row_data_free (RowData *data) +{ + g_object_unref (data->item); + g_free (data->section_id); + g_free (data->section_title); + g_free (data); +} + +static gboolean +transform_binding_to_accel (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + CcKeyboardItem *item; + CcKeyCombo *combo; + gchar *accelerator; + + item = CC_KEYBOARD_ITEM (g_binding_get_source (binding)); + combo = cc_keyboard_item_get_primary_combo (item); + + /* Embolden the label when the shortcut is modified */ + if (!cc_keyboard_item_is_value_default (item)) + { + g_autofree gchar *tmp = NULL; + + tmp = convert_keysym_state_to_string (combo); + + accelerator = g_strdup_printf ("<b>%s</b>", tmp); + } + else + { + accelerator = convert_keysym_state_to_string (combo); + } + + g_value_take_string (to_value, accelerator); + + return TRUE; +} + +static void +shortcut_modified_changed_cb (CcKeyboardItem *item, + GParamSpec *pspec, + GtkWidget *button) +{ + gtk_widget_set_child_visible (button, !cc_keyboard_item_is_value_default (item)); +} + +static void +reset_all_shortcuts_cb (GtkWidget *widget, + gpointer user_data) +{ + CcKeyboardPanel *self; + RowData *data; + + self = user_data; + + if (widget == (GtkWidget *) self->add_shortcut_row) + return; + + data = g_object_get_data (G_OBJECT (widget), "data"); + + /* Don't reset custom shortcuts */ + if (cc_keyboard_item_get_item_type (data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) + return; + + /* cc_keyboard_manager_reset_shortcut() already resets conflicting shortcuts, + * so no other check is needed here. */ + cc_keyboard_manager_reset_shortcut (self->manager, data->item); +} + +static void +reset_all_clicked_cb (CcKeyboardPanel *self) +{ + GtkWidget *dialog, *toplevel, *button; + guint response; + + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + dialog = gtk_message_dialog_new (GTK_WINDOW (toplevel), + GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_NONE, + _("Reset All Shortcuts?")); + + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("Resetting the shortcuts may affect your custom shortcuts. " + "This cannot be undone.")); + + gtk_dialog_add_buttons (GTK_DIALOG (dialog), + _("Cancel"), GTK_RESPONSE_CANCEL, + _("Reset All"), GTK_RESPONSE_ACCEPT, + NULL); + + gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL); + + /* Make the "Reset All" button destructive */ + button = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT); + gtk_style_context_add_class (gtk_widget_get_style_context (button), "destructive-action"); + + /* Reset shortcuts if accepted */ + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + if (response == GTK_RESPONSE_ACCEPT) + { + gtk_container_foreach (GTK_CONTAINER (self->shortcuts_listbox), + reset_all_shortcuts_cb, + self); + } + + gtk_widget_destroy (dialog); +} + +static void +reset_shortcut_cb (GtkWidget *reset_button, + CcKeyboardItem *item) +{ + CcKeyboardPanel *self; + + self = CC_KEYBOARD_PANEL (gtk_widget_get_ancestor (reset_button, CC_TYPE_KEYBOARD_PANEL)); + + cc_keyboard_manager_reset_shortcut (self->manager, item); +} + +static void +add_item (CcKeyboardPanel *self, + CcKeyboardItem *item, + const gchar *section_id, + const gchar *section_title) +{ + GtkWidget *row, *box, *label, *reset_button; + + /* Horizontal box */ + box = g_object_new (GTK_TYPE_BOX, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 18, + "margin-start", 6, + "margin-end", 6, + "margin-bottom", 4, + "margin-top", 4, + NULL); + gtk_widget_show (box); + + /* Shortcut title */ + label = gtk_label_new (cc_keyboard_item_get_description (item)); + gtk_widget_show (label); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_line_wrap (GTK_LABEL (label), TRUE); + gtk_label_set_line_wrap_mode (GTK_LABEL (label), PANGO_WRAP_WORD_CHAR); + gtk_widget_set_hexpand (label, TRUE); + + g_object_bind_property (item, + "description", + label, + "label", + G_BINDING_DEFAULT); + + gtk_container_add (GTK_CONTAINER (box), label); + + /* Shortcut accelerator */ + label = gtk_label_new (""); + gtk_widget_show (label); + gtk_label_set_xalign (GTK_LABEL (label), 0.0); + gtk_label_set_use_markup (GTK_LABEL (label), TRUE); + + gtk_size_group_add_widget (self->accelerator_sizegroup, label); + + g_object_bind_property_full (item, + "binding", + label, + "label", + G_SETTINGS_BIND_GET | G_BINDING_SYNC_CREATE, + transform_binding_to_accel, + NULL, NULL, NULL); + + gtk_container_add (GTK_CONTAINER (box), label); + + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + + /* Reset shortcut button */ + reset_button = gtk_button_new_from_icon_name ("edit-clear-symbolic", GTK_ICON_SIZE_BUTTON); + gtk_widget_show (reset_button); + gtk_widget_set_valign (reset_button, GTK_ALIGN_CENTER); + + gtk_button_set_relief (GTK_BUTTON (reset_button), GTK_RELIEF_NONE); + gtk_widget_set_child_visible (reset_button, !cc_keyboard_item_is_value_default (item)); + + gtk_widget_set_tooltip_text (reset_button, _("Reset the shortcut to its default value")); + + gtk_container_add (GTK_CONTAINER (box), reset_button); + + gtk_style_context_add_class (gtk_widget_get_style_context (reset_button), "flat"); + gtk_style_context_add_class (gtk_widget_get_style_context (reset_button), "circular"); + gtk_style_context_add_class (gtk_widget_get_style_context (reset_button), "reset-shortcut-button"); + + g_signal_connect_object (item, + "notify::is-value-default", + G_CALLBACK (shortcut_modified_changed_cb), + reset_button, 0); + + g_signal_connect_object (reset_button, + "clicked", + G_CALLBACK (reset_shortcut_cb), + item, 0); + + /* The row */ + row = gtk_list_box_row_new (); + gtk_widget_show (row); + gtk_container_add (GTK_CONTAINER (row), box); + + g_object_set_data_full (G_OBJECT (row), + "data", + row_data_new (item, section_id, section_title), + (GDestroyNotify) row_data_free); + + gtk_container_add (GTK_CONTAINER (self->shortcuts_listbox), row); +} + +static void +remove_item (CcKeyboardPanel *self, + CcKeyboardItem *item) +{ + GList *children, *l; + + children = gtk_container_get_children (GTK_CONTAINER (self->shortcuts_listbox)); + + for (l = children; l != NULL; l = l->next) + { + RowData *row_data; + + row_data = g_object_get_data (l->data, "data"); + + if (row_data->item == item) + { + gtk_container_remove (GTK_CONTAINER (self->shortcuts_listbox), l->data); + break; + } + } + + g_list_free (children); +} + +static gboolean +strv_contains_prefix_or_match (gchar **strv, + const gchar *prefix) +{ + guint i; + + const struct { + const gchar *key; + const gchar *untranslated; + const gchar *synonym; + } key_aliases[] = + { + { "ctrl", "Ctrl", "ctrl" }, + { "win", "Super", "super" }, + { "option", NULL, "alt" }, + { "command", NULL, "super" }, + { "apple", NULL, "super" }, + }; + + for (i = 0; strv[i]; i++) + { + if (g_str_has_prefix (strv[i], prefix)) + return TRUE; + } + + for (i = 0; i < G_N_ELEMENTS (key_aliases); i++) + { + g_autofree gchar *alias = NULL; + const gchar *synonym; + + if (!g_str_has_prefix (key_aliases[i].key, prefix)) + continue; + + if (key_aliases[i].untranslated) + { + const gchar *translated_label; + + /* Steal GTK+'s translation */ + translated_label = g_dpgettext2 ("gtk30", "keyboard label", key_aliases[i].untranslated); + alias = g_utf8_strdown (translated_label, -1); + } + + synonym = key_aliases[i].synonym; + + /* If a translation or synonym of the key is in the accelerator, and we typed + * the key, also consider that a prefix */ + if ((alias && g_strv_contains ((const gchar * const *) strv, alias)) || + (synonym && g_strv_contains ((const gchar * const *) strv, synonym))) + { + return TRUE; + } + } + + return FALSE; +} + +static gboolean +search_match_shortcut (CcKeyboardItem *item, + const gchar *search) +{ + CcKeyCombo *combo = cc_keyboard_item_get_primary_combo (item); + GStrv shortcut_tokens, search_tokens; + g_autofree gchar *normalized_accel = NULL; + g_autofree gchar *accel = NULL; + gboolean match; + guint i; + + if (is_empty_binding (combo)) + return FALSE; + + match = TRUE; + accel = convert_keysym_state_to_string (combo); + normalized_accel = cc_util_normalize_casefold_and_unaccent (accel); + + shortcut_tokens = g_strsplit_set (normalized_accel, SHORTCUT_DELIMITERS, -1); + search_tokens = g_strsplit_set (search, SHORTCUT_DELIMITERS, -1); + + for (i = 0; search_tokens[i] != NULL; i++) + { + const gchar *token; + + /* Strip leading and trailing whitespaces */ + token = g_strstrip (search_tokens[i]); + + if (g_utf8_strlen (token, -1) == 0) + continue; + + match = match && strv_contains_prefix_or_match (shortcut_tokens, token); + + if (!match) + break; + } + + g_strfreev (shortcut_tokens); + g_strfreev (search_tokens); + + return match; +} + +static gint +sort_function (GtkListBoxRow *a, + GtkListBoxRow *b, + gpointer user_data) +{ + CcKeyboardPanel *self; + RowData *a_data, *b_data; + gint retval; + + self = user_data; + + if (a == self->add_shortcut_row) + return 1; + + if (b == self->add_shortcut_row) + return -1; + + a_data = g_object_get_data (G_OBJECT (a), "data"); + b_data = g_object_get_data (G_OBJECT (b), "data"); + + /* Put custom shortcuts below everything else */ + if (cc_keyboard_item_get_item_type (a_data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) + return 1; + else if (cc_keyboard_item_get_item_type (b_data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) + return -1; + + retval = g_strcmp0 (a_data->section_title, b_data->section_title); + + if (retval != 0) + return retval; + + return g_strcmp0 (cc_keyboard_item_get_description (a_data->item), cc_keyboard_item_get_description (b_data->item)); +} + +static void +header_function (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + CcKeyboardPanel *self; + gboolean add_header; + RowData *data; + + self = user_data; + add_header = FALSE; + + /* The + row always has a separator */ + if (row == self->add_shortcut_row) + { + GtkWidget *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show (separator); + + gtk_list_box_row_set_header (row, separator); + + return; + } + + data = g_object_get_data (G_OBJECT (row), "data"); + + if (before) + { + RowData *before_data = g_object_get_data (G_OBJECT (before), "data"); + + if (before_data) + add_header = g_strcmp0 (before_data->section_id, data->section_id) != 0; + } + else + { + add_header = TRUE; + } + + if (add_header) + { + GtkWidget *box, *label, *separator; + g_autofree gchar *markup = NULL; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6); + gtk_widget_show (box); + gtk_widget_set_margin_top (box, before ? 18 : 6); + + markup = g_strdup_printf ("<b>%s</b>", _(data->section_title)); + label = g_object_new (GTK_TYPE_LABEL, + "label", markup, + "use-markup", TRUE, + "xalign", 0.0, + "margin-start", 6, + NULL); + gtk_widget_show (label); + gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label"); + gtk_container_add (GTK_CONTAINER (box), label); + + separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show (separator); + gtk_container_add (GTK_CONTAINER (box), separator); + + gtk_list_box_row_set_header (row, box); + } + else + { + gtk_list_box_row_set_header (row, NULL); + } +} + +static gboolean +filter_function (GtkListBoxRow *row, + gpointer user_data) +{ + CcKeyboardPanel *self = user_data; + CcKeyboardItem *item; + RowData *data; + gboolean retval; + g_autofree gchar *search = NULL; + g_autofree gchar *name = NULL; + g_auto(GStrv) terms = NULL; + guint i; + + if (gtk_entry_get_text_length (GTK_ENTRY (self->search_entry)) == 0) + return TRUE; + + /* When searching, the '+' row is always hidden */ + if (row == self->add_shortcut_row) + return FALSE; + + data = g_object_get_data (G_OBJECT (row), "data"); + item = data->item; + name = cc_util_normalize_casefold_and_unaccent (cc_keyboard_item_get_description (item)); + search = cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (self->search_entry))); + terms = g_strsplit (search, " ", -1); + + for (i = 0; terms && terms[i]; i++) + { + retval = strstr (name, terms[i]) || search_match_shortcut (item, terms[i]); + if (!retval) + break; + } + + return retval; +} + +static void +shortcut_row_activated (GtkWidget *button, + GtkListBoxRow *row, + CcKeyboardPanel *self) +{ + CcKeyboardShortcutEditor *editor; + + editor = CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor); + + if (row != self->add_shortcut_row) + { + RowData *data = g_object_get_data (G_OBJECT (row), "data"); + + cc_keyboard_shortcut_editor_set_mode (editor, CC_SHORTCUT_EDITOR_EDIT); + cc_keyboard_shortcut_editor_set_item (editor, data->item); + } + else + { + cc_keyboard_shortcut_editor_set_mode (editor, CC_SHORTCUT_EDITOR_CREATE); + cc_keyboard_shortcut_editor_set_item (editor, NULL); + } + + gtk_widget_show (self->shortcut_editor); +} + +static void +alternate_chars_activated (GtkWidget *button, + GtkListBoxRow *row, + CcKeyboardPanel *self) +{ + GtkWindow *window; + + window = GTK_WINDOW (cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (self)))); + + gtk_window_set_transient_for (GTK_WINDOW (self->alt_chars_key_dialog), window); + gtk_widget_show (GTK_WIDGET (self->alt_chars_key_dialog)); +} + +static gboolean +transform_binding_to_alt_chars (GValue *value, + GVariant *variant, + gpointer user_data) +{ + const char **items; + guint i; + + items = g_variant_get_strv (variant, NULL); + if (!items) + goto bail; + + for (i = 0; items[i] != NULL; i++) + { + guint j; + + if (!g_str_has_prefix (items[i], "lv3:")) + continue; + + for (j = 0; j < G_N_ELEMENTS (lv3_xkb_options); j++) + { + if (!g_str_equal (items[i], lv3_xkb_options[j].xkb_option)) + continue; + + g_value_set_string (value, + g_dpgettext2 (NULL, "keyboard key", lv3_xkb_options[j].label)); + return TRUE; + } + } + +bail: + g_value_set_string (value, + g_dpgettext2 (NULL, "keyboard key", lv3_xkb_options[DEFAULT_LV3_OPTION].label)); + return TRUE; +} + +static void +cc_keyboard_panel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) + { + case PROP_PARAMETERS: + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static const char * +cc_keyboard_panel_get_help_uri (CcPanel *panel) +{ + return "help:gnome-help/keyboard"; +} + +static void +cc_keyboard_panel_finalize (GObject *object) +{ + CcKeyboardPanel *self = CC_KEYBOARD_PANEL (object); + GtkWidget *window; + + g_clear_pointer (&self->pictures_regex, g_regex_unref); + g_clear_object (&self->accelerator_sizegroup); + g_clear_object (&self->input_source_settings); + + cc_keyboard_option_clear_all (); + + if (self->search_bar_handler_id != 0) + { + window = cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (self))); + g_signal_handler_disconnect (window, self->search_bar_handler_id); + } + + G_OBJECT_CLASS (cc_keyboard_panel_parent_class)->finalize (object); +} + +static void +cc_keyboard_panel_constructed (GObject *object) +{ + CcKeyboardPanel *self = CC_KEYBOARD_PANEL (object); + GtkWindow *toplevel; + CcShell *shell; + + G_OBJECT_CLASS (cc_keyboard_panel_parent_class)->constructed (object); + + /* Setup the dialog's transient parent */ + shell = cc_panel_get_shell (CC_PANEL (self)); + toplevel = GTK_WINDOW (cc_shell_get_toplevel (shell)); + gtk_window_set_transient_for (GTK_WINDOW (self->shortcut_editor), toplevel); + + cc_shell_embed_widget_in_header (shell, self->reset_button, GTK_POS_LEFT); + cc_shell_embed_widget_in_header (shell, self->search_button, GTK_POS_RIGHT); + + self->search_bar_handler_id = + g_signal_connect_swapped (toplevel, + "key-press-event", + G_CALLBACK (gtk_search_bar_handle_event), + self->search_bar); +} + +static void +cc_keyboard_panel_class_init (CcKeyboardPanelClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + CcPanelClass *panel_class = CC_PANEL_CLASS (klass); + + panel_class->get_help_uri = cc_keyboard_panel_get_help_uri; + + object_class->set_property = cc_keyboard_panel_set_property; + object_class->finalize = cc_keyboard_panel_finalize; + object_class->constructed = cc_keyboard_panel_constructed; + + g_object_class_override_property (object_class, PROP_PARAMETERS, "parameters"); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-keyboard-panel.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, add_shortcut_row); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, empty_search_placeholder); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, reset_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, search_bar); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, search_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, search_entry); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, shortcuts_listbox); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, value_alternate_chars); + + gtk_widget_class_bind_template_callback (widget_class, reset_all_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, shortcut_row_activated); + gtk_widget_class_bind_template_callback (widget_class, alternate_chars_activated); +} + +static void +cc_keyboard_panel_init (CcKeyboardPanel *self) +{ + GtkCssProvider *provider; + + g_resources_register (cc_keyboard_get_resource ()); + + gtk_widget_init_template (GTK_WIDGET (self)); + + /* Custom CSS */ + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_data (provider, custom_css, -1, NULL); + + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1); + + g_object_unref (provider); + + /* Alternate characters key */ + self->input_source_settings = g_settings_new ("org.gnome.desktop.input-sources"); + g_settings_bind_with_mapping (self->input_source_settings, + "xkb-options", + self->value_alternate_chars, + "label", + G_SETTINGS_BIND_GET, + transform_binding_to_alt_chars, + NULL, + self->value_alternate_chars, + NULL); + + self->alt_chars_key_dialog = cc_alt_chars_key_dialog_new (self->input_source_settings); + + /* Shortcut manager */ + self->manager = cc_keyboard_manager_new (); + + /* Use a sizegroup to make the accelerator labels the same width */ + self->accelerator_sizegroup = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); + + g_signal_connect_object (self->manager, + "shortcut-added", + G_CALLBACK (add_item), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->manager, + "shortcut-removed", + G_CALLBACK (remove_item), + self, + G_CONNECT_SWAPPED); + + cc_keyboard_manager_load_shortcuts (self->manager); + + /* Shortcut editor dialog */ + self->shortcut_editor = cc_keyboard_shortcut_editor_new (self->manager); + + /* Setup the shortcuts shortcuts_listbox */ + gtk_list_box_set_sort_func (GTK_LIST_BOX (self->shortcuts_listbox), + sort_function, + self, + NULL); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->shortcuts_listbox), + header_function, + self, + NULL); + + gtk_list_box_set_filter_func (GTK_LIST_BOX (self->shortcuts_listbox), + filter_function, + self, + NULL); + + gtk_list_box_set_placeholder (GTK_LIST_BOX (self->shortcuts_listbox), self->empty_search_placeholder); +} + diff --git a/panels/keyboard/cc-keyboard-panel.h b/panels/keyboard/cc-keyboard-panel.h new file mode 100644 index 0000000..db6e352 --- /dev/null +++ b/panels/keyboard/cc-keyboard-panel.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010 Intel, 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 <http://www.gnu.org/licenses/>. + * + * Author: Thomas Wood <thomas.wood@intel.com> + * + */ + + +#pragma once + +#include <shell/cc-panel.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_KEYBOARD_PANEL (cc_keyboard_panel_get_type ()) +G_DECLARE_FINAL_TYPE (CcKeyboardPanel, cc_keyboard_panel, CC, KEYBOARD_PANEL, CcPanel) + +CcKeyboardItem* cc_keyboard_panel_create_custom_item (CcKeyboardPanel *self); + +G_END_DECLS diff --git a/panels/keyboard/cc-keyboard-panel.ui b/panels/keyboard/cc-keyboard-panel.ui new file mode 100644 index 0000000..16e522c --- /dev/null +++ b/panels/keyboard/cc-keyboard-panel.ui @@ -0,0 +1,196 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <!-- interface-requires gtk+ 3.0 --> + <object class="GtkAdjustment" id="cursor_blink_time_adjustment"> + <property name="lower">100</property> + <property name="upper">2500</property> + <property name="value">1000</property> + <property name="step_increment">200</property> + <property name="page_increment">200</property> + </object> + <template class="CcKeyboardPanel" parent="CcPanel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="expand">True</property> + <signal name="key-press-event" handler="gtk_search_bar_handle_event" object="search_bar" swapped="yes" /> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkSearchBar" id="search_bar"> + <property name="visible">True</property> + <property name="hexpand">True</property> + <property name="search_mode_enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional" /> + <child> + <object class="GtkSearchEntry" id="search_entry"> + <property name="visible">True</property> + <property name="width_chars">30</property> + <signal name="notify::text" handler="gtk_list_box_invalidate_filter" object="shortcuts_listbox" swapped="yes" /> + </object> + </child> + </object> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="hscrollbar_policy">never</property> + <property name="propagate_natural_width">True</property> + <property name="propagate_natural_height">True</property> + <property name="max_content_height">350</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="margin_top">32</property> + <property name="margin_bottom">32</property> + <property name="margin_left">18</property> + <property name="margin_right">18</property> + <property name="spacing">12</property> + <property name="halign">center</property> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkListBox" id="alternate_chars_listbox"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection-mode">none</property> + <property name="width-request">250</property> + <signal name="row-activated" handler="alternate_chars_activated" object="CcKeyboardPanel" swapped="no" /> + <child> + <object class="HdyActionRow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use-underline">true</property> + <property name="title" translatable="yes">Alternate Characters Key</property> + <property name="subtitle" translatable="yes">Hold down and type to enter different characters</property> + <property name="activatable">True</property> + <child> + <object class="GtkLabel" id="value_alternate_chars"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="no">Right Alt</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkFrame"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkListBox" id="shortcuts_listbox"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="selection-mode">none</property> + <property name="width-request">250</property> + <signal name="row-activated" handler="shortcut_row_activated" object="CcKeyboardPanel" swapped="no" /> + <child> + <object class="GtkListBoxRow" id="add_shortcut_row"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="border_width">6</property> + <child type="center"> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon_name">list-add-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + + <!-- Header widgets --> + <object class="GtkToggleButton" id="search_button"> + <property name="visible">True</property> + <style> + <class name="image-button" /> + </style> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="icon_name">system-search-symbolic</property> + </object> + </child> + </object> + <object class="GtkButton" id="reset_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Reset All…</property> + <property name="tooltip-text" translatable="yes">Reset all shortcuts to their default keybindings</property> + <signal name="clicked" handler="reset_all_clicked_cb" object="CcKeyboardPanel" swapped="yes" /> + </object> + + <object class="GtkBox" id="empty_search_placeholder"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="border_width">18</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="pixel_size">80</property> + <property name="icon_name">edit-find-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">No keyboard shortcut found</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.44"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Try a different search</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> +</interface> diff --git a/panels/keyboard/cc-keyboard-shortcut-editor.c b/panels/keyboard/cc-keyboard-shortcut-editor.c new file mode 100644 index 0000000..e53bb01 --- /dev/null +++ b/panels/keyboard/cc-keyboard-shortcut-editor.c @@ -0,0 +1,1012 @@ +/* cc-keyboard-shortcut-editor.h + * + * Copyright (C) 2016 Endless, 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 <http://www.gnu.org/licenses/>. + * + * Authors: Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + */ + +#include <glib-object.h> +#include <glib/gi18n.h> + +#include "cc-keyboard-shortcut-editor.h" +#include "keyboard-shortcuts.h" + +struct _CcKeyboardShortcutEditor +{ + GtkDialog parent; + + GtkButton *add_button; + GtkButton *cancel_button; + GtkButton *change_custom_shortcut_button; + GtkEntry *command_entry; + GtkGrid *custom_grid; + GtkShortcutLabel *custom_shortcut_accel_label; + GtkStack *custom_shortcut_stack; + GtkBox *edit_box; + GtkHeaderBar *headerbar; + GtkEntry *name_entry; + GtkLabel *new_shortcut_conflict_label; + GtkButton *remove_button; + GtkButton *replace_button; + GtkButton *reset_button; + GtkButton *reset_custom_button; + GtkButton *set_button; + GtkShortcutLabel *shortcut_accel_label; + GtkLabel *shortcut_conflict_label; + GtkBox *standard_box; + GtkStack *stack; + GtkLabel *top_info_label; + + CcShortcutEditorMode mode; + + CcKeyboardManager *manager; + CcKeyboardItem *item; + GBinding *reset_item_binding; + + CcKeyboardItem *collision_item; + + /* Custom shortcuts */ + GdkDevice *grab_pointer; + guint grab_idle_id; + + CcKeyCombo *custom_combo; + gboolean custom_is_modifier; + gboolean edited : 1; +}; + +static void command_entry_changed_cb (CcKeyboardShortcutEditor *self); +static void name_entry_changed_cb (CcKeyboardShortcutEditor *self); +static void set_button_clicked_cb (CcKeyboardShortcutEditor *self); + +G_DEFINE_TYPE (CcKeyboardShortcutEditor, cc_keyboard_shortcut_editor, GTK_TYPE_DIALOG) + +enum +{ + PROP_0, + PROP_KEYBOARD_ITEM, + PROP_MANAGER, + N_PROPS +}; + +typedef enum +{ + HEADER_MODE_NONE, + HEADER_MODE_ADD, + HEADER_MODE_SET, + HEADER_MODE_REPLACE, + HEADER_MODE_CUSTOM_CANCEL, + HEADER_MODE_CUSTOM_EDIT +} HeaderMode; + +typedef enum +{ + PAGE_CUSTOM, + PAGE_EDIT, + PAGE_STANDARD, +} ShortcutEditorPage; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + +/* Getter and setter for ShortcutEditorPage */ +static ShortcutEditorPage +get_shortcut_editor_page (CcKeyboardShortcutEditor *self) +{ + if (gtk_stack_get_visible_child (self->stack) == GTK_WIDGET (self->edit_box)) + return PAGE_EDIT; + + if (gtk_stack_get_visible_child (self->stack) == GTK_WIDGET (self->custom_grid)) + return PAGE_CUSTOM; + + return PAGE_STANDARD; +} + +static void +set_shortcut_editor_page (CcKeyboardShortcutEditor *self, + ShortcutEditorPage page) +{ + switch (page) + { + case PAGE_CUSTOM: + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->custom_grid)); + break; + + case PAGE_EDIT: + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->edit_box)); + break; + + case PAGE_STANDARD: + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->standard_box)); + break; + + default: + g_assert_not_reached (); + } + + gtk_widget_set_visible (GTK_WIDGET (self->top_info_label), page != PAGE_CUSTOM); +} + +static void +apply_custom_item_fields (CcKeyboardShortcutEditor *self, + CcKeyboardItem *item) +{ + /* Only setup the binding when it was actually edited */ + if (self->edited) + { + CcKeyCombo *combo = cc_keyboard_item_get_primary_combo (item); + g_autofree gchar *binding = NULL; + + combo->keycode = self->custom_combo->keycode; + combo->keyval = self->custom_combo->keyval; + combo->mask = self->custom_combo->mask; + + if (combo->keycode == 0 && combo->keyval == 0 && combo->mask == 0) + binding = g_strdup (""); + else + binding = gtk_accelerator_name_with_keycode (NULL, + combo->keyval, + combo->keycode, + combo->mask); + + g_object_set (G_OBJECT (item), "binding", binding, NULL); + } + + /* Set the keyboard shortcut name and command for custom entries */ + if (cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) + { + g_settings_set_string (cc_keyboard_item_get_settings (item), "name", gtk_entry_get_text (self->name_entry)); + g_settings_set_string (cc_keyboard_item_get_settings (item), "command", gtk_entry_get_text (self->command_entry)); + } +} + +static void +clear_custom_entries (CcKeyboardShortcutEditor *self) +{ + g_signal_handlers_block_by_func (self->command_entry, command_entry_changed_cb, self); + g_signal_handlers_block_by_func (self->name_entry, name_entry_changed_cb, self); + + gtk_entry_set_text (self->name_entry, ""); + gtk_entry_set_text (self->command_entry, ""); + + gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->custom_shortcut_accel_label), ""); + gtk_label_set_label (self->new_shortcut_conflict_label, ""); + gtk_label_set_label (self->shortcut_conflict_label, ""); + + memset (self->custom_combo, 0, sizeof (CcKeyCombo)); + self->custom_is_modifier = TRUE; + self->edited = FALSE; + + self->collision_item = NULL; + + g_signal_handlers_unblock_by_func (self->command_entry, command_entry_changed_cb, self); + g_signal_handlers_unblock_by_func (self->name_entry, name_entry_changed_cb, self); +} + +static void +cancel_editing (CcKeyboardShortcutEditor *self) +{ + cc_keyboard_shortcut_editor_set_item (self, NULL); + clear_custom_entries (self); + + gtk_widget_hide (GTK_WIDGET (self)); +} + +static gboolean +is_custom_shortcut (CcKeyboardShortcutEditor *self) { + return self->item == NULL || cc_keyboard_item_get_item_type (self->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH; +} + +static void +grab_seat (CcKeyboardShortcutEditor *self) +{ + GdkGrabStatus status; + GdkWindow *window; + GdkSeat *seat; + + window = gtk_widget_get_window (GTK_WIDGET (self)); + g_assert (window); + + seat = gdk_display_get_default_seat (gdk_window_get_display (window)); + + status = gdk_seat_grab (seat, + window, + GDK_SEAT_CAPABILITY_KEYBOARD, + FALSE, + NULL, + NULL, + NULL, + NULL); + + if (status != GDK_GRAB_SUCCESS) { + g_warning ("Grabbing keyboard failed"); + return; + } + + self->grab_pointer = gdk_seat_get_keyboard (seat); + if (!self->grab_pointer) + self->grab_pointer = gdk_seat_get_pointer (seat); + + gtk_grab_add (GTK_WIDGET (self)); +} + +static void +release_grab (CcKeyboardShortcutEditor *self) +{ + if (self->grab_pointer) + { + gdk_seat_ungrab (gdk_device_get_seat (self->grab_pointer)); + self->grab_pointer = NULL; + + gtk_grab_remove (GTK_WIDGET (self)); + } +} + +static void +update_shortcut (CcKeyboardShortcutEditor *self) +{ + if (!self->item) + return; + + /* Setup the binding */ + apply_custom_item_fields (self, self->item); + + /* Eventually disable the conflict shortcut */ + if (self->collision_item) + cc_keyboard_manager_disable_shortcut (self->manager, self->collision_item); + + /* Cleanup whatever was set before */ + clear_custom_entries (self); + + cc_keyboard_shortcut_editor_set_item (self, NULL); +} + +static GtkShortcutLabel* +get_current_shortcut_label (CcKeyboardShortcutEditor *self) +{ + if (is_custom_shortcut (self)) + return GTK_SHORTCUT_LABEL (self->custom_shortcut_accel_label); + + return GTK_SHORTCUT_LABEL (self->shortcut_accel_label); +} + +static void +set_header_mode (CcKeyboardShortcutEditor *self, + HeaderMode mode) +{ + gtk_header_bar_set_show_close_button (self->headerbar, mode == HEADER_MODE_CUSTOM_EDIT); + + gtk_widget_set_visible (GTK_WIDGET (self->add_button), mode == HEADER_MODE_ADD); + gtk_widget_set_visible (GTK_WIDGET (self->cancel_button), mode != HEADER_MODE_NONE && + mode != HEADER_MODE_CUSTOM_EDIT); + gtk_widget_set_visible (GTK_WIDGET (self->replace_button), mode == HEADER_MODE_REPLACE); + gtk_widget_set_visible (GTK_WIDGET (self->set_button), mode == HEADER_MODE_SET); + gtk_widget_set_visible (GTK_WIDGET (self->remove_button), mode == HEADER_MODE_CUSTOM_EDIT); + + /* By setting the default response, the action button gets the 'suggested-action' applied */ + switch (mode) + { + case HEADER_MODE_SET: + gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_APPLY); + break; + + case HEADER_MODE_REPLACE: + gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT); + break; + + case HEADER_MODE_ADD: + gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_OK); + break; + + default: + gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_NONE); + } +} + +static void +setup_custom_shortcut (CcKeyboardShortcutEditor *self) +{ + GtkShortcutLabel *shortcut_label; + CcKeyboardItem *collision_item; + HeaderMode mode; + gboolean is_custom, is_accel_empty; + gboolean valid, accel_valid; + gchar *accel; + + is_custom = is_custom_shortcut (self); + accel_valid = is_valid_binding (self->custom_combo) && + is_valid_accel (self->custom_combo) && + !self->custom_is_modifier; + + is_accel_empty = is_empty_binding (self->custom_combo); + + if (is_accel_empty) + accel_valid = TRUE; + valid = accel_valid; + + /* Additional checks for custom shortcuts */ + if (is_custom) + { + if (accel_valid) + { + set_shortcut_editor_page (self, PAGE_CUSTOM); + + /* We have to check if the current accelerator is empty in order to + * decide if we show the "Set Shortcut" button or the accelerator label */ + gtk_stack_set_visible_child (self->custom_shortcut_stack, + is_accel_empty ? GTK_WIDGET (self->change_custom_shortcut_button) : GTK_WIDGET (self->custom_shortcut_accel_label)); + gtk_widget_set_visible (GTK_WIDGET (self->reset_custom_button), !is_accel_empty); + } + + valid = accel_valid && + gtk_entry_get_text_length (self->name_entry) > 0 && + gtk_entry_get_text_length (self->command_entry) > 0; + } + + gtk_widget_set_sensitive (GTK_WIDGET (self->replace_button), valid); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), valid); + if (valid) + set_header_mode (self, HEADER_MODE_ADD); + else + set_header_mode (self, is_custom ? HEADER_MODE_CUSTOM_CANCEL : HEADER_MODE_NONE); + + /* Nothing else to do if the shortcut is invalid */ + if (!accel_valid) + return; + + /* Valid shortcut, show it in the standard page */ + if (!is_custom) + set_shortcut_editor_page (self, PAGE_STANDARD); + + shortcut_label = get_current_shortcut_label (self); + + collision_item = cc_keyboard_manager_get_collision (self->manager, + self->item, + self->custom_combo); + + accel = gtk_accelerator_name (self->custom_combo->keyval, self->custom_combo->mask); + + + /* Setup the accelerator label */ + gtk_shortcut_label_set_accelerator (shortcut_label, accel); + + self->edited = TRUE; + + release_grab (self); + + /* + * Oops! Looks like the accelerator is already being used, so we + * must warn the user and let it be very clear that adding this + * shortcut will disable the other. + */ + gtk_widget_set_visible (GTK_WIDGET (self->new_shortcut_conflict_label), collision_item != NULL); + + if (collision_item) + { + GtkLabel *label; + g_autofree gchar *friendly_accelerator = NULL; + g_autofree gchar *accelerator_text = NULL; + g_autofree gchar *collision_text = NULL; + + friendly_accelerator = convert_keysym_state_to_string (self->custom_combo); + + accelerator_text = g_strdup_printf ("<b>%s</b>", friendly_accelerator); + collision_text = g_strdup_printf (_("%s is already being used for %s. If you " + "replace it, %s will be disabled"), + accelerator_text, + cc_keyboard_item_get_description (collision_item), + cc_keyboard_item_get_description (collision_item)); + + label = is_custom_shortcut (self) ? self->new_shortcut_conflict_label : self->shortcut_conflict_label; + + gtk_label_set_markup (label, collision_text); + } + + /* + * When there is a collision between the current shortcut and another shortcut, + * and we're editing an existing shortcut (rather than creating a new one), setup + * the headerbar to display "Cancel" and "Replace". Otherwise, make sure to set + * only the close button again. + */ + if (collision_item) + { + mode = HEADER_MODE_REPLACE; + } + else + { + if (self->mode == CC_SHORTCUT_EDITOR_EDIT) + mode = is_custom ? HEADER_MODE_CUSTOM_EDIT : HEADER_MODE_SET; + else + mode = is_custom ? HEADER_MODE_ADD : HEADER_MODE_SET; + } + + set_header_mode (self, mode); + + self->collision_item = collision_item; +} + +static void +add_button_clicked_cb (CcKeyboardShortcutEditor *self) +{ + CcKeyboardItem *item; + + item = cc_keyboard_manager_create_custom_shortcut (self->manager); + + /* Apply the custom shortcut setup at the new item */ + apply_custom_item_fields (self, item); + + /* Eventually disable the conflict shortcut */ + if (self->collision_item) + cc_keyboard_manager_disable_shortcut (self->manager, self->collision_item); + + /* Cleanup everything once we're done */ + clear_custom_entries (self); + + cc_keyboard_manager_add_custom_shortcut (self->manager, item); + + gtk_widget_hide (GTK_WIDGET (self)); +} + +static void +cancel_button_clicked_cb (GtkWidget *button, + CcKeyboardShortcutEditor *self) +{ + cancel_editing (self); +} + +static void +change_custom_shortcut_button_clicked_cb (CcKeyboardShortcutEditor *self) +{ + grab_seat (self); + set_shortcut_editor_page (self, PAGE_EDIT); + set_header_mode (self, HEADER_MODE_NONE); +} + +static void +command_entry_changed_cb (CcKeyboardShortcutEditor *self) +{ + setup_custom_shortcut (self); +} + +static void +name_entry_changed_cb (CcKeyboardShortcutEditor *self) +{ + setup_custom_shortcut (self); +} + +static void +remove_button_clicked_cb (CcKeyboardShortcutEditor *self) +{ + gtk_widget_hide (GTK_WIDGET (self)); + + cc_keyboard_manager_remove_custom_shortcut (self->manager, self->item); +} + +static void +replace_button_clicked_cb (CcKeyboardShortcutEditor *self) +{ + if (self->mode == CC_SHORTCUT_EDITOR_CREATE) + add_button_clicked_cb (self); + else + set_button_clicked_cb (self); +} + +static void +reset_custom_clicked_cb (CcKeyboardShortcutEditor *self) +{ + if (self->item) + cc_keyboard_manager_reset_shortcut (self->manager, self->item); + + gtk_stack_set_visible_child (self->custom_shortcut_stack, GTK_WIDGET (self->change_custom_shortcut_button)); + gtk_widget_hide (GTK_WIDGET (self->reset_custom_button)); +} + +static void +reset_item_clicked_cb (CcKeyboardShortcutEditor *self) +{ + CcKeyCombo *combo; + gchar *accel; + + /* Reset first, then update the shortcut */ + cc_keyboard_manager_reset_shortcut (self->manager, self->item); + + combo = cc_keyboard_item_get_primary_combo (self->item); + accel = gtk_accelerator_name (combo->keyval, combo->mask); + gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->shortcut_accel_label), accel); + + g_free (accel); +} + +static void +set_button_clicked_cb (CcKeyboardShortcutEditor *self) +{ + update_shortcut (self); + gtk_widget_hide (GTK_WIDGET (self)); +} + +static void +setup_keyboard_item (CcKeyboardShortcutEditor *self, + CcKeyboardItem *item) +{ + CcKeyCombo *combo; + gboolean is_custom; + g_autofree gchar *accel = NULL; + g_autofree gchar *description_text = NULL; + g_autofree gchar *text = NULL; + + if (!item) { + gtk_label_set_text (self->top_info_label, _("Enter the new shortcut")); + return; + } + + combo = cc_keyboard_item_get_primary_combo (item); + is_custom = cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH; + accel = gtk_accelerator_name (combo->keyval, combo->mask); + + /* To avoid accidentally thinking we unset the current keybinding, set the values + * of the keyboard item that is being edited */ + self->custom_is_modifier = FALSE; + self->custom_combo->keycode = combo->keycode; + self->custom_combo->keyval = combo->keyval; + self->custom_combo->mask = combo->mask; + + /* Headerbar */ + gtk_header_bar_set_title (self->headerbar, + is_custom ? _("Set Custom Shortcut") : _("Set Shortcut")); + + set_header_mode (self, is_custom ? HEADER_MODE_CUSTOM_EDIT : HEADER_MODE_NONE); + + gtk_widget_hide (GTK_WIDGET (self->add_button)); + gtk_widget_hide (GTK_WIDGET (self->cancel_button)); + gtk_widget_hide (GTK_WIDGET (self->replace_button)); + + /* Setup the top label */ + description_text = g_strdup_printf ("<b>%s</b>", cc_keyboard_item_get_description (item)); + /* TRANSLATORS: %s is replaced with a description of the keyboard shortcut */ + text = g_strdup_printf (_("Enter new shortcut to change %s."), description_text); + + gtk_label_set_markup (self->top_info_label, text); + + /* Accelerator labels */ + gtk_shortcut_label_set_accelerator (self->shortcut_accel_label, accel); + gtk_shortcut_label_set_accelerator (self->custom_shortcut_accel_label, accel); + + g_clear_pointer (&self->reset_item_binding, g_binding_unbind); + self->reset_item_binding = g_object_bind_property (item, + "is-value-default", + self->reset_button, + "visible", + G_BINDING_DEFAULT | G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE); + + /* Setup the custom entries */ + if (is_custom) + { + gboolean is_accel_empty; + + g_signal_handlers_block_by_func (self->command_entry, command_entry_changed_cb, self); + g_signal_handlers_block_by_func (self->name_entry, name_entry_changed_cb, self); + + /* Name entry */ + gtk_entry_set_text (self->name_entry, cc_keyboard_item_get_description (item)); + gtk_widget_set_sensitive (GTK_WIDGET (self->name_entry), cc_keyboard_item_get_desc_editable (item)); + + /* Command entry */ + gtk_entry_set_text (self->command_entry, cc_keyboard_item_get_command (item)); + gtk_widget_set_sensitive (GTK_WIDGET (self->command_entry), cc_keyboard_item_get_cmd_editable (item)); + + /* If there is no accelerator set for this custom shortcut, show the "Set Shortcut" button. */ + is_accel_empty = !accel || accel[0] == '\0'; + + gtk_stack_set_visible_child (self->custom_shortcut_stack, + is_accel_empty ? GTK_WIDGET (self->change_custom_shortcut_button) : GTK_WIDGET (self->custom_shortcut_accel_label)); + + gtk_widget_set_visible (GTK_WIDGET (self->reset_custom_button), !is_accel_empty); + + g_signal_handlers_unblock_by_func (self->command_entry, command_entry_changed_cb, self); + g_signal_handlers_unblock_by_func (self->name_entry, name_entry_changed_cb, self); + + release_grab (self); + } + + /* Show the appropriate view */ + set_shortcut_editor_page (self, is_custom ? PAGE_CUSTOM : PAGE_EDIT); +} + +static void +cc_keyboard_shortcut_editor_finalize (GObject *object) +{ + CcKeyboardShortcutEditor *self = (CcKeyboardShortcutEditor *)object; + + g_clear_object (&self->item); + g_clear_object (&self->manager); + + g_clear_pointer (&self->custom_combo, g_free); + g_clear_pointer (&self->reset_item_binding, g_binding_unbind); + + G_OBJECT_CLASS (cc_keyboard_shortcut_editor_parent_class)->finalize (object); +} + +static void +cc_keyboard_shortcut_editor_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (object); + + switch (prop_id) + { + case PROP_KEYBOARD_ITEM: + g_value_set_object (value, self->item); + break; + + case PROP_MANAGER: + g_value_set_object (value, self->manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_keyboard_shortcut_editor_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (object); + + switch (prop_id) + { + case PROP_KEYBOARD_ITEM: + cc_keyboard_shortcut_editor_set_item (self, g_value_get_object (value)); + break; + + case PROP_MANAGER: + g_set_object (&self->manager, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static gboolean +cc_keyboard_shortcut_editor_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + CcKeyboardShortcutEditor *self; + GdkModifierType real_mask; + gboolean editing; + guint keyval_lower; + + self = CC_KEYBOARD_SHORTCUT_EDITOR (widget); + + /* Being in the "change-shortcut" page is the only check we must + * perform to decide if we're editing a shortcut. */ + editing = get_shortcut_editor_page (self) == PAGE_EDIT; + + if (!editing) + return GTK_WIDGET_CLASS (cc_keyboard_shortcut_editor_parent_class)->key_press_event (widget, event); + + real_mask = event->state & gtk_accelerator_get_default_mod_mask (); + + keyval_lower = gdk_keyval_to_lower (event->keyval); + + /* Normalise <Tab> */ + if (keyval_lower == GDK_KEY_ISO_Left_Tab) + keyval_lower = GDK_KEY_Tab; + + /* Put shift back if it changed the case of the key, not otherwise. */ + if (keyval_lower != event->keyval) + real_mask |= GDK_SHIFT_MASK; + + if (keyval_lower == GDK_KEY_Sys_Req && + (real_mask & GDK_MOD1_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 */ + keyval_lower = GDK_KEY_Print; + } + + /* A single Escape press cancels the editing */ + if (!event->is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_Escape) + { + self->edited = FALSE; + + release_grab (self); + cancel_editing (self); + + return GDK_EVENT_STOP; + } + + /* Backspace disables the current shortcut */ + if (!event->is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_BackSpace) + { + self->edited = TRUE; + self->custom_is_modifier = FALSE; + memset (self->custom_combo, 0, sizeof (CcKeyCombo)); + + gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->custom_shortcut_accel_label), ""); + gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->shortcut_accel_label), ""); + + release_grab (self); + + self->edited = FALSE; + + setup_custom_shortcut (self); + + return GDK_EVENT_STOP; + } + + self->custom_is_modifier = event->is_modifier; + self->custom_combo->keycode = event->hardware_keycode; + self->custom_combo->keyval = keyval_lower; + self->custom_combo->mask = real_mask; + + /* CapsLock isn't supported as a keybinding modifier, so keep it from confusing us */ + self->custom_combo->mask &= ~GDK_LOCK_MASK; + + setup_custom_shortcut (self); + + return GDK_EVENT_STOP; +} + +static void +cc_keyboard_shortcut_editor_close (GtkDialog *dialog) +{ + CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (dialog); + + if (self->mode == CC_SHORTCUT_EDITOR_EDIT) + update_shortcut (self); + + GTK_DIALOG_CLASS (cc_keyboard_shortcut_editor_parent_class)->close (dialog); +} + +static void +cc_keyboard_shortcut_editor_response (GtkDialog *dialog, + gint response_id) +{ + CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (dialog); + + if (response_id == GTK_RESPONSE_DELETE_EVENT && + self->mode == CC_SHORTCUT_EDITOR_EDIT) + { + update_shortcut (self); + } +} + +static gboolean +grab_idle (gpointer data) +{ + CcKeyboardShortcutEditor *self = data; + + if (self->item && cc_keyboard_item_get_item_type (self->item) != CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) + grab_seat (self); + + self->grab_idle_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +cc_keyboard_shortcut_editor_show (GtkWidget *widget) +{ + CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (widget); + + /* Map before grabbing, so that the window is visible */ + GTK_WIDGET_CLASS (cc_keyboard_shortcut_editor_parent_class)->show (widget); + + self->grab_idle_id = g_timeout_add (100, grab_idle, self); +} + +static void +cc_keyboard_shortcut_editor_unrealize (GtkWidget *widget) +{ + CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (widget); + + if (self->grab_idle_id) { + g_source_remove (self->grab_idle_id); + self->grab_idle_id = 0; + } + + release_grab (self); + + GTK_WIDGET_CLASS (cc_keyboard_shortcut_editor_parent_class)->unrealize (widget); +} + +static void +cc_keyboard_shortcut_editor_class_init (CcKeyboardShortcutEditorClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkDialogClass *dialog_class = GTK_DIALOG_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = cc_keyboard_shortcut_editor_finalize; + object_class->get_property = cc_keyboard_shortcut_editor_get_property; + object_class->set_property = cc_keyboard_shortcut_editor_set_property; + + widget_class->show = cc_keyboard_shortcut_editor_show; + widget_class->unrealize = cc_keyboard_shortcut_editor_unrealize; + widget_class->key_press_event = cc_keyboard_shortcut_editor_key_press_event; + + dialog_class->close = cc_keyboard_shortcut_editor_close; + dialog_class->response = cc_keyboard_shortcut_editor_response; + + /** + * CcKeyboardShortcutEditor:keyboard-item: + * + * The current keyboard shortcut being edited. + */ + properties[PROP_KEYBOARD_ITEM] = g_param_spec_object ("keyboard-item", + "Keyboard item", + "The keyboard item being edited", + CC_TYPE_KEYBOARD_ITEM, + G_PARAM_READWRITE); + + /** + * CcKeyboardShortcutEditor:panel: + * + * The current keyboard panel. + */ + properties[PROP_MANAGER] = g_param_spec_object ("manager", + "Keyboard manager", + "The keyboard manager", + CC_TYPE_KEYBOARD_MANAGER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-keyboard-shortcut-editor.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, add_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, cancel_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, change_custom_shortcut_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, command_entry); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, custom_grid); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, custom_shortcut_accel_label); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, custom_shortcut_stack); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, edit_box); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, headerbar); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, name_entry); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, new_shortcut_conflict_label); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, remove_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, replace_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, reset_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, reset_custom_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, set_button); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, shortcut_accel_label); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, shortcut_conflict_label); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, standard_box); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, stack); + gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, top_info_label); + + gtk_widget_class_bind_template_callback (widget_class, add_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, change_custom_shortcut_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, command_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, name_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, remove_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, replace_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, reset_custom_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, reset_item_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, set_button_clicked_cb); +} + +static void +cc_keyboard_shortcut_editor_init (CcKeyboardShortcutEditor *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->mode = CC_SHORTCUT_EDITOR_EDIT; + self->custom_is_modifier = TRUE; + self->custom_combo = g_new0 (CcKeyCombo, 1); + + gtk_widget_set_direction (GTK_WIDGET (self->custom_shortcut_accel_label), GTK_TEXT_DIR_LTR); + gtk_widget_set_direction (GTK_WIDGET (self->shortcut_accel_label), GTK_TEXT_DIR_LTR); +} + +/** + * cc_keyboard_shortcut_editor_new: + * + * Creates a new #CcKeyboardShortcutEditor. + * + * Returns: (transfer full): a newly created #CcKeyboardShortcutEditor. + */ +GtkWidget* +cc_keyboard_shortcut_editor_new (CcKeyboardManager *manager) +{ + return g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_EDITOR, + "manager", manager, + "use-header-bar", 1, + NULL); +} + +/** + * cc_keyboard_shortcut_editor_get_item: + * @self: a #CcKeyboardShortcutEditor + * + * Retrieves the current keyboard shortcut being edited. + * + * Returns: (transfer none)(nullable): a #CcKeyboardItem + */ +CcKeyboardItem* +cc_keyboard_shortcut_editor_get_item (CcKeyboardShortcutEditor *self) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self), NULL); + + return self->item; +} + +/** + * cc_keyboard_shortcut_editor_set_item: + * @self: a #CcKeyboardShortcutEditor + * @item: a #CcKeyboardItem + * + * Sets the current keyboard shortcut to be edited. + */ +void +cc_keyboard_shortcut_editor_set_item (CcKeyboardShortcutEditor *self, + CcKeyboardItem *item) +{ + g_return_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self)); + + setup_keyboard_item (self, item); + + if (!g_set_object (&self->item, item)) + return; + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_KEYBOARD_ITEM]); +} + +CcShortcutEditorMode +cc_keyboard_shortcut_editor_get_mode (CcKeyboardShortcutEditor *self) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self), 0); + + return self->mode; +} + +void +cc_keyboard_shortcut_editor_set_mode (CcKeyboardShortcutEditor *self, + CcShortcutEditorMode mode) +{ + gboolean is_create_mode; + + g_return_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self)); + + self->mode = mode; + is_create_mode = mode == CC_SHORTCUT_EDITOR_CREATE; + + gtk_widget_set_visible (GTK_WIDGET (self->new_shortcut_conflict_label), is_create_mode); + gtk_stack_set_visible_child (self->custom_shortcut_stack, + is_create_mode ? GTK_WIDGET (self->change_custom_shortcut_button) : GTK_WIDGET (self->custom_shortcut_accel_label)); + + if (mode == CC_SHORTCUT_EDITOR_CREATE) + { + /* Cleanup whatever was set before */ + clear_custom_entries (self); + + set_header_mode (self, HEADER_MODE_ADD); + set_shortcut_editor_page (self, PAGE_CUSTOM); + gtk_header_bar_set_title (self->headerbar, _("Add Custom Shortcut")); + + gtk_widget_set_sensitive (GTK_WIDGET (self->command_entry), TRUE); + gtk_widget_set_sensitive (GTK_WIDGET (self->name_entry), TRUE); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE); + + gtk_widget_hide (GTK_WIDGET (self->reset_custom_button)); + } +} diff --git a/panels/keyboard/cc-keyboard-shortcut-editor.h b/panels/keyboard/cc-keyboard-shortcut-editor.h new file mode 100644 index 0000000..963309f --- /dev/null +++ b/panels/keyboard/cc-keyboard-shortcut-editor.h @@ -0,0 +1,52 @@ +/* cc-keyboard-shortcut-editor.h + * + * Copyright (C) 2016 Endless, 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 <http://www.gnu.org/licenses/>. + * + * Authors: Georges Basile Stavracas Neto <georges.stavracas@gmail.com> + */ + +#pragma once + +#include <gtk/gtk.h> + +#include "cc-keyboard-item.h" +#include "cc-keyboard-manager.h" + +G_BEGIN_DECLS + +#define CC_TYPE_KEYBOARD_SHORTCUT_EDITOR (cc_keyboard_shortcut_editor_get_type ()) +G_DECLARE_FINAL_TYPE (CcKeyboardShortcutEditor, cc_keyboard_shortcut_editor, CC, KEYBOARD_SHORTCUT_EDITOR, GtkDialog) + +typedef enum +{ + CC_SHORTCUT_EDITOR_CREATE, + CC_SHORTCUT_EDITOR_EDIT +} CcShortcutEditorMode; + +GtkWidget* cc_keyboard_shortcut_editor_new (CcKeyboardManager *manager); + +CcKeyboardItem* cc_keyboard_shortcut_editor_get_item (CcKeyboardShortcutEditor *self); + +void cc_keyboard_shortcut_editor_set_item (CcKeyboardShortcutEditor *self, + CcKeyboardItem *item); + +CcShortcutEditorMode cc_keyboard_shortcut_editor_get_mode (CcKeyboardShortcutEditor *self); + +void cc_keyboard_shortcut_editor_set_mode (CcKeyboardShortcutEditor *self, + CcShortcutEditorMode mode); + +G_END_DECLS + diff --git a/panels/keyboard/cc-keyboard-shortcut-editor.ui b/panels/keyboard/cc-keyboard-shortcut-editor.ui new file mode 100644 index 0000000..e904709 --- /dev/null +++ b/panels/keyboard/cc-keyboard-shortcut-editor.ui @@ -0,0 +1,354 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="3.20"/> + <template class="CcKeyboardShortcutEditor" parent="GtkDialog"> + <property name="can_focus">False</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="width_request">400</property> + <property name="height_request">300</property> + <property name="window_position">center</property> + <property name="type_hint">dialog</property> + <signal name="close" handler="cancel_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="no" /> + <signal name="delete-event" handler="gtk_widget_hide_on_delete" object="CcKeyboardShortcutEditor" swapped="yes"/> + <child internal-child="vbox"> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <property name="border_width">12</property> + <child> + <object class="GtkLabel" id="top_info_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="width_chars">15</property> + <property name="max_width_chars">20</property> + </object> + <packing> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <child> + <object class="GtkBox" id="edit_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <property name="expand">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="resource">/org/gnome/control-center/keyboard/enter-keyboard-shortcut.svg</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="wrap">True</property> + <property name="label" translatable="yes">Press Esc to cancel or Backspace to disable the keyboard shortcut.</property> + <style> + <class name="dim-label" /> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="standard_box"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <property name="spacing">18</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">18</property> + <child type="center"> + <object class="GtkShortcutLabel" id="shortcut_accel_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="disabled-text" translatable="yes">Disabled</property> + </object> + </child> + <child> + <object class="GtkButton" id="reset_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="relief">none</property> + <property name="halign">end</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon-name">edit-clear-symbolic</property> + </object> + </child> + <signal name="clicked" handler="reset_item_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="pack_type">end</property> + </packing> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="shortcut_conflict_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="width_chars">15</property> + <property name="max_width_chars">20</property> + <property name="xalign">0</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkGrid" id="custom_grid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="row_spacing">12</property> + <property name="column_spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Name</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Command</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Shortcut</property> + <property name="xalign">1</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="new_shortcut_conflict_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="hexpand">True</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <property name="width_chars">15</property> + <property name="max_width_chars">20</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="name_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <signal name="notify::text" handler="name_entry_changed_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="command_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <signal name="notify::text" handler="command_entry_changed_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + <property name="width">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="reset_custom_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="relief">none</property> + <property name="halign">end</property> + <child> + <object class="GtkImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="icon-name">edit-clear-symbolic</property> + </object> + </child> + <signal name="clicked" handler="reset_custom_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkStack" id="custom_shortcut_stack"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButton" id="change_custom_shortcut_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="label" translatable="yes">Set Shortcut…</property> + <signal name="clicked" handler="change_custom_shortcut_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + </child> + <child> + <object class="GtkShortcutLabel" id="custom_shortcut_accel_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="hexpand">True</property> + <property name="disabled-text" translatable="yes">None</property> + </object> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <child type="titlebar"> + <object class="GtkHeaderBar" id="headerbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="show_close_button">True</property> + <child> + <object class="GtkButton" id="cancel_button"> + <property name="label" translatable="yes">Cancel</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="cancel_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="no" /> + </object> + </child> + <child> + <object class="GtkButton" id="remove_button"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Remove</property> + <property name="valign">end</property> + <signal name="clicked" handler="remove_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + <style> + <class name="destructive-action" /> + </style> + </object> + </child> + <child> + <object class="GtkButton" id="add_button"> + <property name="label" translatable="yes">Add</property> + <property name="visible">True</property> + <property name="sensitive">False</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="add_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="pack_type">end</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="replace_button"> + <property name="label" translatable="yes">Replace</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="replace_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="pack_type">end</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkButton" id="set_button"> + <property name="label" translatable="yes">Set</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <signal name="clicked" handler="set_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" /> + </object> + <packing> + <property name="pack_type">end</property> + <property name="position">3</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="cancel">cancel_button</action-widget> + <action-widget response="accept">replace_button</action-widget> + <action-widget response="apply">set_button</action-widget> + <action-widget response="ok" default="true">add_button</action-widget> + </action-widgets> + </template> + <object class="GtkSizeGroup"> + <widgets> + <widget name="cancel_button"/> + <widget name="add_button"/> + <widget name="replace_button"/> + <widget name="reset_button"/> + </widgets> + </object> +</interface> diff --git a/panels/keyboard/enter-keyboard-shortcut.svg b/panels/keyboard/enter-keyboard-shortcut.svg new file mode 100644 index 0000000..b7ce2e4 --- /dev/null +++ b/panels/keyboard/enter-keyboard-shortcut.svg @@ -0,0 +1,245 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="256" + height="72" + viewBox="0 0 256 72.000001" + id="svg3611" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="enter-keyboard-shortcut.svg"> + <defs + id="defs3613" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="2.8" + inkscape:cx="137.98997" + inkscape:cy="34.663602" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:window-width="1366" + inkscape:window-height="704" + inkscape:window-x="0" + inkscape:window-y="27" + inkscape:window-maximized="1" /> + <metadata + id="metadata3616"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Camada 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-980.36216)"> + <g + id="g3715" + transform="translate(-503.23415,689.94658)"> + <path + d="m 509.66363,325.47627 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19813 1.43306,0.67185 1.50029,1.50028 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50028 z" + id="path27275" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 543.62146,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.5003,-1.50028 z" + id="path27277" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 577.57927,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.6952,-1.70681 1.5003,-1.50028 z" + id="path27279" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 611.12326,325.47627 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19813 1.43306,0.67185 1.50029,1.50028 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50028 z" + id="path5218" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 645.08109,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 1.99751,-13.05863 0.12565,-0.82161 0.69519,-1.70681 1.50031,-1.50028 z" + id="path5220" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 679.0389,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 1.99751,-13.05863 0.12565,-0.82161 0.69521,-1.70681 1.50031,-1.50028 z" + id="path5222" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 712.58289,325.47627 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19813 1.43306,0.67185 1.50029,1.50028 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33167,-8.70575 1.99751,-13.05863 0.12565,-0.82161 0.69519,-1.70681 1.50029,-1.50028 z" + id="path4829" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.60000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 516.19044,335.26648 c 5.53002,1.41851 18.51389,1.41851 24.2936,0 0.8072,-0.19812 1.43306,0.67186 1.50028,1.50029 l 1.99752,13.05863 -1.50029,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50029 z" + id="path3662" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 550.14827,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29358,0 0.8072,-0.19812 1.43307,0.67186 1.50028,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.5003,-1.50029 z" + id="path3664" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 584.10608,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29357,0 0.80721,-0.19812 1.43308,0.67186 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.6952,-1.70681 1.5003,-1.50029 z" + id="path3666" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 617.65007,335.26648 c 5.53002,1.41851 18.51389,1.41851 24.29359,0 0.80721,-0.19812 1.43306,0.67186 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50029 z" + id="path3668" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 651.6079,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29357,0 0.80721,-0.19812 1.43308,0.67186 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.5003,-1.50029 z" + id="path3670" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 685.56571,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29357,0 0.80721,-0.19812 1.43308,0.67186 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.6952,-1.70681 1.5003,-1.50029 z" + id="path3672" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 719.1097,335.26648 c 5.53002,1.41851 18.51389,1.41851 24.29359,0 0.80721,-0.19812 1.43306,0.67186 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50029 z" + id="path3674" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.60000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 525.98066,345.0567 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19812 1.43306,0.67185 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33167,-8.70576 1.99752,-13.05863 0.12564,-0.82162 0.69518,-1.70681 1.50028,-1.50029 z" + id="path3676" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 559.93849,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19812 1.43308,0.67185 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70576 1.99751,-13.05863 0.12565,-0.82162 0.69519,-1.70681 1.50031,-1.50029 z" + id="path3678" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 593.8963,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19812 1.43308,0.67185 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70576 1.99751,-13.05863 0.12565,-0.82162 0.69521,-1.70681 1.50031,-1.50029 z" + id="path3680" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 627.44029,345.0567 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19812 1.43306,0.67185 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33167,-8.70576 1.99751,-13.05863 0.12565,-0.82162 0.69519,-1.70681 1.50029,-1.50029 z" + id="path3682" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 661.39812,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19812 1.43308,0.67185 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28863,0 -1.50028,0 c 0.66583,-4.35288 1.33168,-8.70576 1.99751,-13.05863 0.12564,-0.82162 0.69519,-1.70681 1.50031,-1.50029 z" + id="path3684" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 695.35593,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.8072,-0.19812 1.43308,0.67185 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28863,0 -1.50028,0 c 0.66583,-4.35288 1.33168,-8.70576 1.99751,-13.05863 0.12564,-0.82162 0.69521,-1.70681 1.50031,-1.50029 z" + id="path3686" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <path + d="m 728.89992,345.0567 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.8072,-0.19812 1.43306,0.67185 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28863,0 -1.50028,0 c 0.66583,-4.35288 1.33166,-8.70576 1.99751,-13.05863 0.12564,-0.82162 0.69519,-1.70681 1.50029,-1.50029 z" + id="path3688" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.60000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + sodipodi:nodetypes="sssccccss" + inkscape:connector-curvature="0" /> + <g + id="g3713" + transform="matrix(1.359752,0,0,1.359752,418.09336,-671.08525)"> + <path + d="m 95.250257,715.10933 0,1.09089 c -1.31e-4,0.0113 -5.02e-4,0.0227 0,0.0341 0.01222,0.27812 0.140266,0.55621 0.340902,0.74999 l 5.693061,5.76124 5.65897,-5.76124 c 0.20529,-0.20532 0.30681,-0.49473 0.30681,-0.78413 l 0,-1.09089 -1.09088,0 c -0.28941,0 -0.57881,0.10156 -0.78408,0.30681 l -4.09082,4.15901 -4.124913,-4.15901 c -0.212319,-0.22989 -0.511898,-0.33071 -0.818164,-0.30681 l -1.090886,0 z" + id="path3715" + style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale Mono';-inkscape-font-specification:'Andale Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new" + inkscape:connector-curvature="0" /> + <rect + height="11.999745" + id="rect3717" + rx="0" + ry="0" + style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" + transform="scale(-1,1)" + width="2.1817718" + x="-102.34102" + y="708.92743" /> + </g> + <g + id="g3740" + transform="matrix(1.359752,0,0,1.359752,492.12198,-661.29504)"> + <path + d="m 95.250257,715.10933 0,1.09089 c -1.31e-4,0.0113 -5.02e-4,0.0227 0,0.0341 0.01222,0.27812 0.140266,0.55621 0.340902,0.74999 l 5.693061,5.76124 5.65897,-5.76124 c 0.20529,-0.20532 0.30681,-0.49473 0.30681,-0.78413 l 0,-1.09089 -1.09088,0 c -0.28941,0 -0.57881,0.10156 -0.78408,0.30681 l -4.09082,4.15901 -4.124913,-4.15901 c -0.212319,-0.22989 -0.511898,-0.33071 -0.818164,-0.30681 l -1.090886,0 z" + id="path3742" + style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale Mono';-inkscape-font-specification:'Andale Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new" + inkscape:connector-curvature="0" /> + <rect + height="11.999745" + id="rect3744" + rx="0" + ry="0" + style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" + transform="scale(-1,1)" + width="2.1817718" + x="-102.34102" + y="708.92743" /> + </g> + <g + id="g3746" + transform="matrix(1.359752,0,0,1.359752,593.58161,-661.29504)"> + <path + d="m 95.250257,715.10933 0,1.09089 c -1.31e-4,0.0113 -5.02e-4,0.0227 0,0.0341 0.01222,0.27812 0.140266,0.55621 0.340902,0.74999 l 5.693061,5.76124 5.65897,-5.76124 c 0.20529,-0.20532 0.30681,-0.49473 0.30681,-0.78413 l 0,-1.09089 -1.09088,0 c -0.28941,0 -0.57881,0.10156 -0.78408,0.30681 l -4.09082,4.15901 -4.124913,-4.15901 c -0.212319,-0.22989 -0.511898,-0.33071 -0.818164,-0.30681 l -1.090886,0 z" + id="path3748" + style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale Mono';-inkscape-font-specification:'Andale Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new" + inkscape:connector-curvature="0" /> + <rect + height="11.999745" + id="rect3750" + rx="0" + ry="0" + style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate" + transform="scale(-1,1)" + width="2.1817718" + x="-102.34102" + y="708.92743" /> + </g> + </g> + </g> +</svg> diff --git a/panels/keyboard/gnome-keybindings.pc.in b/panels/keyboard/gnome-keybindings.pc.in new file mode 100644 index 0000000..e099b4c --- /dev/null +++ b/panels/keyboard/gnome-keybindings.pc.in @@ -0,0 +1,10 @@ +prefix=@prefix@ +datarootdir=@datarootdir@ +datadir=@datadir@ +pkgdatadir=${datadir}/@PACKAGE@ +keysdir=${pkgdatadir}/keybindings + +Name: gnome-keybindings +Description: Keybindings configuration for GNOME applications +Version: @VERSION@ + diff --git a/panels/keyboard/gnome-keyboard-panel.desktop.in.in b/panels/keyboard/gnome-keyboard-panel.desktop.in.in new file mode 100644 index 0000000..9200d69 --- /dev/null +++ b/panels/keyboard/gnome-keyboard-panel.desktop.in.in @@ -0,0 +1,18 @@ +[Desktop Entry] +Name=Keyboard Shortcuts +Comment=View and change keyboard shortcuts and set your typing preferences +Exec=gnome-control-center keyboard +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=input-keyboard +Terminal=false +Type=Application +NoDisplay=true +StartupNotify=true +Categories=GNOME;GTK;Settings;HardwareSettings;X-GNOME-Settings-Panel;X-GNOME-DevicesSettings; +OnlyShowIn=GNOME;Unity; +X-GNOME-Bugzilla-Bugzilla=GNOME +X-GNOME-Bugzilla-Product=gnome-control-center +X-GNOME-Bugzilla-Component=keyboard +X-GNOME-Bugzilla-Version=@VERSION@ +# Translators: Search terms to find the Keyboard panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Shortcut;Workspace;Window;Resize;Zoom;Contrast;Input;Source;Lock;Volume; diff --git a/panels/keyboard/keyboard-shortcuts.c b/panels/keyboard/keyboard-shortcuts.c new file mode 100644 index 0000000..ba748a6 --- /dev/null +++ b/panels/keyboard/keyboard-shortcuts.c @@ -0,0 +1,379 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: Thomas Wood <thomas.wood@intel.com> + * Rodrigo Moya <rodrigo@gnome.org> + * Christophe Fergeau <cfergeau@redhat.com> + */ + +#include <config.h> + +#include <glib/gi18n.h> + +#include "keyboard-shortcuts.h" +#include "cc-keyboard-option.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 (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 (CcKeyCombo *combo) +{ + if (combo->keyval == 0 && + combo->mask == 0 && + combo->keycode == 0) + return TRUE; + return FALSE; +} + +gboolean +is_valid_accel (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 (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; +} diff --git a/panels/keyboard/keyboard-shortcuts.h b/panels/keyboard/keyboard-shortcuts.h new file mode 100644 index 0000000..e303486 --- /dev/null +++ b/panels/keyboard/keyboard-shortcuts.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 Intel, 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 <http://www.gnu.org/licenses/>. + * + * Authors: Thomas Wood <thomas.wood@intel.com> + * Rodrigo Moya <rodrigo@gnome.org> + */ + +#include <gtk/gtk.h> +#include <shell/cc-panel.h> + +#include "cc-keyboard-item.h" + +typedef struct { + /* The untranslated name, combine with ->package to translate */ + char *name; + /* The group of keybindings (system or application) */ + char *group; + /* The gettext package to use to translate the section title */ + char *package; + /* Name of the window manager the keys would apply to */ + char *wm_name; + /* The GSettings schema for the whole file, if any */ + char *schema; + /* an array of KeyListEntry */ + GArray *entries; +} KeyList; + +typedef struct +{ + CcKeyboardItemType type; + char *schema; /* GSettings schema name, if any */ + char *description; /* description for GSettings types */ + char *name; /* GSettings schema path, or GSettings key name depending on type */ + char *reverse_entry; + gboolean is_reversed; + gboolean hidden; +} KeyListEntry; + +typedef struct { + CcKeyboardItem *orig_item; + CcKeyboardItem *conflict_item; + guint new_keyval; + GdkModifierType new_mask; + guint new_keycode; +} CcUniquenessData; + +enum +{ + SECTION_DESCRIPTION_COLUMN, + SECTION_ID_COLUMN, + SECTION_GROUP_COLUMN, + SECTION_N_COLUMNS +}; + +gchar* find_free_settings_path (GSettings *settings); + +gboolean is_valid_binding (CcKeyCombo *combo); + +gboolean is_empty_binding (CcKeyCombo *combo); + +gboolean is_valid_accel (CcKeyCombo *combo); + +KeyList* parse_keylist_from_file (const gchar *path); + +gchar* convert_keysym_state_to_string (CcKeyCombo *combo); diff --git a/panels/keyboard/keyboard.gresource.xml b/panels/keyboard/keyboard.gresource.xml new file mode 100644 index 0000000..0283dae --- /dev/null +++ b/panels/keyboard/keyboard.gresource.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/control-center/keyboard"> + <file preprocess="xml-stripblanks">enter-keyboard-shortcut.svg</file> + <file preprocess="xml-stripblanks">cc-alt-chars-key-dialog.ui</file> + <file preprocess="xml-stripblanks">cc-keyboard-panel.ui</file> + <file preprocess="xml-stripblanks">cc-keyboard-shortcut-editor.ui</file> + </gresource> +</gresources> diff --git a/panels/keyboard/meson.build b/panels/keyboard/meson.build new file mode 100644 index 0000000..2f61ed0 --- /dev/null +++ b/panels/keyboard/meson.build @@ -0,0 +1,94 @@ +panels_list += cappletname +desktop = 'gnome-@0@-panel.desktop'.format(cappletname) + +desktop_in = configure_file( + input: desktop + '.in.in', + output: desktop + '.in', + configuration: desktop_conf +) + +i18n.merge_file( + desktop, + type: 'desktop', + input: desktop_in, + output: desktop, + po_dir: po_dir, + install: true, + install_dir: control_center_desktopdir +) + +pc_conf = configuration_data() +pc_conf.set('prefix', control_center_prefix) +pc_conf.set('datarootdir', control_center_datadir) +pc_conf.set('datadir', control_center_datadir) +pc_conf.set('PACKAGE', meson.project_name()) +pc_conf.set('VERSION', meson.project_version()) + +pc = 'gnome-keybindings.pc' + +configure_file( + input: pc + '.in', + output: pc, + install: true, + install_dir: join_paths(control_center_datadir, 'pkgconfig'), + configuration: pc_conf +) + +xml_files = [ + '00-multimedia.xml', + '01-input-sources.xml', + '01-launchers.xml', + '01-screenshot.xml', + '01-system.xml', + '50-accessibility.xml' +] + +foreach file: xml_files + i18n.merge_file( + file, + input: file + '.in', + output: file, + po_dir: po_dir, + data_dirs: its_dir, + install: true, + install_dir: join_paths(control_center_pkgdatadir, 'keybindings') + ) +endforeach + +sources = files( + 'cc-alt-chars-key-dialog.c', + 'cc-keyboard-panel.c', + 'cc-keyboard-item.c', + 'cc-keyboard-manager.c', + 'cc-keyboard-option.c', + 'cc-keyboard-shortcut-editor.c', + 'wm-common.c', + 'keyboard-shortcuts.c' +) + +resource_data = files( + 'enter-keyboard-shortcut.svg', + 'cc-keyboard-panel.ui', + 'cc-keyboard-shortcut-editor.ui', +) + +sources += gnome.compile_resources( + 'cc-' + cappletname + '-resources', + cappletname + '.gresource.xml', + c_name: 'cc_' + cappletname, + dependencies: resource_data, + export: true +) + +deps = common_deps + [ + gnome_desktop_dep, + x11_dep +] + +panels_libs += static_library( + cappletname, + sources: sources, + include_directories: [top_inc, common_inc], + dependencies: deps, + c_args: cflags +) diff --git a/panels/keyboard/wm-common.c b/panels/keyboard/wm-common.c new file mode 100644 index 0000000..0cddb10 --- /dev/null +++ b/panels/keyboard/wm-common.c @@ -0,0 +1,261 @@ +#include <X11/Xatom.h> +#include <gdk/gdkx.h> +#include <gdk/gdk.h> +#include <string.h> +#include <glib.h> +#include <glib-object.h> +#include "wm-common.h" + +typedef struct _WMCallbackData +{ + GFunc func; + gpointer data; +} WMCallbackData; + +/* Our WM Window */ +static Window wm_window = None; + +/* + * These push/pop implementations are based on the GDK versions, except that they + * use only non-deprecated API. + */ + +static GPtrArray* +push_error_traps (void) +{ + GdkDisplayManager *manager; + g_autoptr(GPtrArray) trapped_displays = NULL; + g_autoptr(GSList) displays = NULL; + GSList *l; + + manager = gdk_display_manager_get (); + displays = gdk_display_manager_list_displays (manager); + trapped_displays = g_ptr_array_new (); + + for (l = displays; l != NULL; l = l->next) + { + GdkDisplay *display = l->data; + +#ifdef GDK_WINDOWING_X11 + if (GDK_IS_X11_DISPLAY (display)) + { + gdk_x11_display_error_trap_push (display); + g_ptr_array_add (trapped_displays, display); + } +#endif + } + + return g_steal_pointer (&trapped_displays); +} + +static gint +pop_error_traps (GPtrArray *displays) +{ + guint i; + gint result; + + result = 0; + + for (i = 0; displays && i < displays->len; i++) + { + GdkDisplay *display; + gint code = 0; + + display = g_ptr_array_index (displays, i); + +#ifdef GDK_WINDOWING_X11 + code = gdk_x11_display_error_trap_pop (display); +#endif + + if (code != 0) + result = code; + } + + return result; +} + +static char * +wm_common_get_window_manager_property (Atom atom) +{ + g_autoptr(GPtrArray) trapped_displays = NULL; + Atom utf8_string, type; + int result; + char *retval; + int format; + gulong nitems; + gulong bytes_after; + gchar *val; + + if (wm_window == None) + return NULL; + + utf8_string = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "UTF8_STRING", False); + + trapped_displays = push_error_traps (); + + val = NULL; + result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), + wm_window, + atom, + 0, G_MAXLONG, + False, utf8_string, + &type, &format, &nitems, + &bytes_after, (guchar **) &val); + + if (pop_error_traps (trapped_displays) || + result != Success || + type != utf8_string || + format != 8 || + nitems == 0 || + !g_utf8_validate (val, nitems, NULL)) + { + retval = NULL; + } + else + { + retval = g_strndup (val, nitems); + } + + g_clear_pointer (&val, XFree); + + return retval; +} +static gchar* +wm_common_get_current_window_manager (void) +{ + Atom atom = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_NET_WM_NAME", False); + char *result; + + result = wm_common_get_window_manager_property (atom); + if (result) + return result; + else + return g_strdup (WM_COMMON_UNKNOWN); +} + +GStrv +wm_common_get_current_keybindings (void) +{ + g_autofree gchar* keybindings = NULL; + g_auto(GStrv) results = NULL; + Atom keybindings_atom; + + keybindings_atom = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_GNOME_WM_KEYBINDINGS", False); + keybindings = wm_common_get_window_manager_property (keybindings_atom); + + if (keybindings) + { + GStrv p; + + results = g_strsplit (keybindings, ",", -1); + + for (p = results; p && *p; p++) + g_strstrip (*p); + } + else + { + g_autofree gchar *wm_name = NULL; + Atom wm_atom; + gchar *to_copy[2] = { NULL, NULL }; + + wm_atom = XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_NET_WM_NAME", False); + wm_name = wm_common_get_window_manager_property (wm_atom); + + to_copy[0] = wm_name ? wm_name : WM_COMMON_UNKNOWN; + + results = g_strdupv (to_copy); + } + + return g_steal_pointer (&results); +} + +static void +update_wm_window (void) +{ + g_autoptr(GPtrArray) trapped_displays = NULL; + Window *xwindow; + Atom type; + gint format; + gulong nitems; + gulong bytes_after; + + XGetWindowProperty (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), GDK_ROOT_WINDOW (), + XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_NET_SUPPORTING_WM_CHECK", False), + 0, G_MAXLONG, False, XA_WINDOW, &type, &format, + &nitems, &bytes_after, (guchar **) &xwindow); + + if (type != XA_WINDOW) + { + wm_window = None; + return; + } + + trapped_displays = push_error_traps (); + + XSelectInput (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), *xwindow, StructureNotifyMask | PropertyChangeMask); + XSync (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), False); + + if (pop_error_traps (trapped_displays)) + { + XFree (xwindow); + wm_window = None; + return; + } + + wm_window = *xwindow; + XFree (xwindow); +} + +static GdkFilterReturn +wm_window_event_filter (GdkXEvent *xev, + GdkEvent *event, + gpointer data) +{ + WMCallbackData *ncb_data = (WMCallbackData*) data; + XEvent *xevent = (XEvent *)xev; + + if ((xevent->type == DestroyNotify && + wm_window != None && xevent->xany.window == wm_window) || + (xevent->type == PropertyNotify && + xevent->xany.window == GDK_ROOT_WINDOW () && + xevent->xproperty.atom == (XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_NET_SUPPORTING_WM_CHECK", False))) || + (xevent->type == PropertyNotify && + wm_window != None && xevent->xany.window == wm_window && + xevent->xproperty.atom == (XInternAtom (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), "_NET_WM_NAME", False)))) + { + update_wm_window (); + (* ncb_data->func) ((gpointer) wm_common_get_current_window_manager (), ncb_data->data); + } + + return GDK_FILTER_CONTINUE; +} + +gpointer +wm_common_register_window_manager_change (GFunc func, + gpointer data) +{ + WMCallbackData *ncb_data; + + ncb_data = g_new0 (WMCallbackData, 1); + + ncb_data->func = func; + ncb_data->data = data; + + gdk_window_add_filter (NULL, wm_window_event_filter, ncb_data); + + update_wm_window (); + + XSelectInput (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), GDK_ROOT_WINDOW (), PropertyChangeMask); + XSync (GDK_DISPLAY_XDISPLAY (gdk_display_get_default ()), False); + + return ncb_data; +} + +void +wm_common_unregister_window_manager_change (gpointer id) +{ + g_return_if_fail (id != NULL); + + gdk_window_remove_filter (NULL, wm_window_event_filter, id); + g_free (id); +} diff --git a/panels/keyboard/wm-common.h b/panels/keyboard/wm-common.h new file mode 100644 index 0000000..461c057 --- /dev/null +++ b/panels/keyboard/wm-common.h @@ -0,0 +1,14 @@ +#pragma once + +#define WM_COMMON_METACITY "Metacity" +#define WM_COMMON_SAWFISH "Sawfish" +#define WM_COMMON_UNKNOWN "Unknown" + +/* Returns a strv of keybinding names for the window manager; + * using _GNOME_WM_KEYBINDINGS if available, _NET_WM_NAME otherwise. */ +GStrv wm_common_get_current_keybindings (void); + +gpointer wm_common_register_window_manager_change (GFunc func, + gpointer data); + +void wm_common_unregister_window_manager_change (gpointer id); |