summaryrefslogtreecommitdiffstats
path: root/panels/keyboard
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:45:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:45:20 +0000
commitae1c76ff830d146d41e88d6fba724c0a54bce868 (patch)
tree3c354bec95af07be35fc71a4b738268496f1a1c4 /panels/keyboard
parentInitial commit. (diff)
downloadgnome-control-center-ae1c76ff830d146d41e88d6fba724c0a54bce868.tar.xz
gnome-control-center-ae1c76ff830d146d41e88d6fba724c0a54bce868.zip
Adding upstream version 1:43.6.upstream/1%43.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'panels/keyboard')
-rw-r--r--panels/keyboard/00-multimedia.xml.in27
-rw-r--r--panels/keyboard/01-input-sources.xml.in15
-rw-r--r--panels/keyboard/01-launchers.xml.in19
-rw-r--r--panels/keyboard/01-system.xml.in9
-rw-r--r--panels/keyboard/50-accessibility.xml.in20
-rw-r--r--panels/keyboard/cc-ibus-utils.c43
-rw-r--r--panels/keyboard/cc-ibus-utils.h26
-rw-r--r--panels/keyboard/cc-input-chooser.c1087
-rw-r--r--panels/keyboard/cc-input-chooser.h41
-rw-r--r--panels/keyboard/cc-input-chooser.ui102
-rw-r--r--panels/keyboard/cc-input-list-box.c821
-rw-r--r--panels/keyboard/cc-input-list-box.h43
-rw-r--r--panels/keyboard/cc-input-list-box.ui47
-rw-r--r--panels/keyboard/cc-input-row.c306
-rw-r--r--panels/keyboard/cc-input-row.h41
-rw-r--r--panels/keyboard/cc-input-row.ui60
-rw-r--r--panels/keyboard/cc-input-source-ibus.c155
-rw-r--r--panels/keyboard/cc-input-source-ibus.h46
-rw-r--r--panels/keyboard/cc-input-source-xkb.c134
-rw-r--r--panels/keyboard/cc-input-source-xkb.h39
-rw-r--r--panels/keyboard/cc-input-source.c84
-rw-r--r--panels/keyboard/cc-input-source.h49
-rw-r--r--panels/keyboard/cc-keyboard-item.c881
-rw-r--r--panels/keyboard/cc-keyboard-item.h112
-rw-r--r--panels/keyboard/cc-keyboard-manager.c1050
-rw-r--r--panels/keyboard/cc-keyboard-manager.h54
-rw-r--r--panels/keyboard/cc-keyboard-panel.c266
-rw-r--r--panels/keyboard/cc-keyboard-panel.h32
-rw-r--r--panels/keyboard/cc-keyboard-panel.ui116
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-dialog.c877
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-dialog.h35
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-dialog.ui227
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-editor.c985
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-editor.h52
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-editor.ui288
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-row.c139
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-row.h38
-rw-r--r--panels/keyboard/cc-keyboard-shortcut-row.ui38
-rw-r--r--panels/keyboard/cc-xkb-modifier-dialog.c355
-rw-r--r--panels/keyboard/cc-xkb-modifier-dialog.h49
-rw-r--r--panels/keyboard/cc-xkb-modifier-dialog.ui73
-rw-r--r--panels/keyboard/enter-keyboard-shortcut.svg245
-rw-r--r--panels/keyboard/gnome-keybindings.pc.in10
-rw-r--r--panels/keyboard/gnome-keyboard-panel.desktop.in.in18
-rw-r--r--panels/keyboard/icons/meson.build4
-rw-r--r--panels/keyboard/icons/scalable/org.gnome.Settings-keyboard-symbolic.svg4
-rw-r--r--panels/keyboard/keyboard-shortcuts.c450
-rw-r--r--panels/keyboard/keyboard-shortcuts.h84
-rw-r--r--panels/keyboard/keyboard.gresource.xml14
-rw-r--r--panels/keyboard/meson.build106
50 files changed, 9816 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-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-ibus-utils.c b/panels/keyboard/cc-ibus-utils.c
new file mode 100644
index 0000000..424c69e
--- /dev/null
+++ b/panels/keyboard/cc-ibus-utils.c
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 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>
+
+#ifdef HAVE_IBUS
+#include "cc-ibus-utils.h"
+
+gchar *
+engine_get_display_name (IBusEngineDesc *engine_desc)
+{
+ const gchar *name;
+ const gchar *language_code;
+ const gchar *language;
+ const gchar *textdomain;
+ gchar *display_name;
+
+ name = ibus_engine_desc_get_longname (engine_desc);
+ language_code = ibus_engine_desc_get_language (engine_desc);
+ language = ibus_get_language_name (language_code);
+ textdomain = ibus_engine_desc_get_textdomain (engine_desc);
+ if (*textdomain != '\0' && *name != '\0')
+ name = g_dgettext (textdomain, name);
+ display_name = g_strdup_printf ("%s (%s)", language, name);
+
+ return display_name;
+}
+
+#endif /* HAVE_IBUS */
diff --git a/panels/keyboard/cc-ibus-utils.h b/panels/keyboard/cc-ibus-utils.h
new file mode 100644
index 0000000..20a0516
--- /dev/null
+++ b/panels/keyboard/cc-ibus-utils.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013 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 <ibus.h>
+
+G_BEGIN_DECLS
+
+gchar *engine_get_display_name (IBusEngineDesc *engine_desc);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-input-chooser.c b/panels/keyboard/cc-input-chooser.c
new file mode 100644
index 0000000..728eb6d
--- /dev/null
+++ b/panels/keyboard/cc-input-chooser.c
@@ -0,0 +1,1087 @@
+/*
+ * Copyright © 2013 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 <locale.h>
+#include <glib/gi18n.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-languages.h>
+
+#include "cc-common-language.h"
+#include "cc-util.h"
+#include "cc-input-chooser.h"
+#include "cc-input-source-ibus.h"
+#include "cc-input-source-xkb.h"
+
+#ifdef HAVE_IBUS
+#include <ibus.h>
+#include "cc-ibus-utils.h"
+#endif /* HAVE_IBUS */
+
+#define INPUT_SOURCE_TYPE_XKB "xkb"
+#define INPUT_SOURCE_TYPE_IBUS "ibus"
+
+#define FILTER_TIMEOUT 150 /* ms */
+
+typedef enum
+{
+ ROW_TRAVEL_DIRECTION_NONE,
+ ROW_TRAVEL_DIRECTION_FORWARD,
+ ROW_TRAVEL_DIRECTION_BACKWARD
+} RowTravelDirection;
+
+typedef enum
+{
+ ROW_LABEL_POSITION_START,
+ ROW_LABEL_POSITION_CENTER,
+ ROW_LABEL_POSITION_END
+} RowLabelPosition;
+
+struct _CcInputChooser
+{
+ GtkDialog parent_instance;
+
+ GtkButton *add_button;
+ GtkSearchEntry *filter_entry;
+ GtkListBox *input_sources_listbox;
+ GtkLabel *login_label;
+ GtkListBoxRow *more_row;
+ GtkWidget *no_results;
+ GtkAdjustment *scroll_adjustment;
+
+ GnomeXkbInfo *xkb_info;
+ GHashTable *ibus_engines;
+ GHashTable *locales;
+ GHashTable *locales_by_language;
+ gboolean showing_extra;
+ guint filter_timeout_id;
+ gchar **filter_words;
+
+ gboolean is_login;
+};
+
+G_DEFINE_TYPE (CcInputChooser, cc_input_chooser, GTK_TYPE_DIALOG)
+
+typedef struct
+{
+ gchar *id;
+ gchar *name;
+ gchar *unaccented_name;
+ gchar *untranslated_name;
+ GtkListBoxRow *default_input_source_row;
+ GtkListBoxRow *locale_row;
+ GtkListBoxRow *back_row;
+ GHashTable *layout_rows_by_id;
+ GHashTable *engine_rows_by_id;
+} LocaleInfo;
+
+static void
+locale_info_free (gpointer data)
+{
+ LocaleInfo *info = data;
+
+ g_free (info->id);
+ g_free (info->name);
+ g_free (info->unaccented_name);
+ g_free (info->untranslated_name);
+ g_clear_object (&info->default_input_source_row);
+ g_clear_object (&info->locale_row);
+ g_clear_object (&info->back_row);
+ g_hash_table_destroy (info->layout_rows_by_id);
+ g_hash_table_destroy (info->engine_rows_by_id);
+ g_free (info);
+}
+
+static void
+set_row_widget_margins (GtkWidget *widget)
+{
+ gtk_widget_set_margin_start (widget, 20);
+ gtk_widget_set_margin_end (widget, 20);
+ gtk_widget_set_margin_top (widget, 6);
+ gtk_widget_set_margin_bottom (widget, 6);
+}
+
+static GtkWidget *
+padded_label_new (const gchar *text,
+ RowLabelPosition position,
+ RowTravelDirection direction,
+ gboolean dim_label)
+{
+ GtkWidget *widget;
+ GtkWidget *label;
+ GtkWidget *arrow;
+ GtkAlign alignment;
+
+ if (position == ROW_LABEL_POSITION_START)
+ alignment = GTK_ALIGN_START;
+ else if (position == ROW_LABEL_POSITION_CENTER)
+ alignment = GTK_ALIGN_CENTER;
+ else
+ alignment = GTK_ALIGN_END;
+
+ widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+
+ if (direction == ROW_TRAVEL_DIRECTION_BACKWARD)
+ {
+ arrow = gtk_image_new_from_icon_name ("go-previous-symbolic");
+ gtk_box_append (GTK_BOX (widget), arrow);
+ }
+
+ label = gtk_label_new (text);
+ gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_MIDDLE);
+ gtk_widget_set_hexpand (label, TRUE);
+ gtk_widget_set_halign (label, alignment);
+ set_row_widget_margins (label);
+ gtk_box_append (GTK_BOX (widget), label);
+ if (dim_label)
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+
+ if (direction == ROW_TRAVEL_DIRECTION_FORWARD)
+ {
+ arrow = gtk_image_new_from_icon_name ("go-next-symbolic");
+ gtk_box_append (GTK_BOX (widget), arrow);
+ }
+
+ return widget;
+}
+
+static GtkListBoxRow *
+more_row_new (void)
+{
+ GtkWidget *row;
+ GtkWidget *box;
+ GtkWidget *arrow;
+
+ row = gtk_list_box_row_new ();
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box);
+ gtk_widget_set_tooltip_text (row, _("More…"));
+
+ arrow = gtk_image_new_from_icon_name ("view-more-symbolic");
+ gtk_widget_set_hexpand (arrow, TRUE);
+ set_row_widget_margins (arrow);
+ gtk_box_append (GTK_BOX (box), arrow);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkWidget *
+no_results_widget_new (void)
+{
+ return padded_label_new (_("No input sources found"), ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, TRUE);
+}
+
+static GtkListBoxRow *
+back_row_new (const gchar *text)
+{
+ GtkWidget *row;
+ GtkWidget *widget;
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_BACKWARD, TRUE);
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), widget);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkListBoxRow *
+locale_row_new (const gchar *text)
+{
+ GtkWidget *row;
+ GtkWidget *widget;
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (text, ROW_LABEL_POSITION_CENTER, ROW_TRAVEL_DIRECTION_NONE, FALSE);
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), widget);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static GtkListBoxRow *
+input_source_row_new (CcInputChooser *self,
+ const gchar *type,
+ const gchar *id)
+{
+ GtkWidget *row = NULL;
+ GtkWidget *widget;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
+ {
+ const gchar *display_name;
+
+ gnome_xkb_info_get_layout_info (self->xkb_info, id, &display_name, NULL, NULL, NULL);
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (display_name,
+ ROW_LABEL_POSITION_START,
+ ROW_TRAVEL_DIRECTION_NONE,
+ FALSE);
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), widget);
+ g_object_set_data (G_OBJECT (row), "name", (gpointer) display_name);
+ g_object_set_data_full (G_OBJECT (row), "unaccented-name",
+ cc_util_normalize_casefold_and_unaccent (display_name), g_free);
+ }
+ else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS))
+ {
+#ifdef HAVE_IBUS
+ gchar *display_name;
+ GtkWidget *image;
+
+ display_name = engine_get_display_name (g_hash_table_lookup (self->ibus_engines, id));
+
+ row = gtk_list_box_row_new ();
+ widget = padded_label_new (display_name,
+ ROW_LABEL_POSITION_START,
+ ROW_TRAVEL_DIRECTION_NONE,
+ FALSE);
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), widget);
+
+ image = gtk_image_new_from_icon_name ("system-run-symbolic");
+ set_row_widget_margins (image);
+ gtk_box_append (GTK_BOX (widget), image);
+
+ g_object_set_data_full (G_OBJECT (row), "name", display_name, g_free);
+ g_object_set_data_full (G_OBJECT (row), "unaccented-name",
+ cc_util_normalize_casefold_and_unaccent (display_name), g_free);
+#else
+ widget = NULL;
+#endif /* HAVE_IBUS */
+ }
+
+ if (row)
+ {
+ g_object_set_data (G_OBJECT (row), "type", (gpointer) type);
+ g_object_set_data (G_OBJECT (row), "id", (gpointer) id);
+
+ return GTK_LIST_BOX_ROW (row);
+ }
+
+ return NULL;
+}
+
+static void
+remove_all_rows (GtkListBox *listbox)
+{
+ GtkWidget *child;
+
+ while ((child = gtk_widget_get_first_child (GTK_WIDGET (listbox))) != NULL)
+ gtk_list_box_remove (listbox, child);
+}
+
+static void
+add_input_source_rows_for_locale (CcInputChooser *self,
+ LocaleInfo *info)
+{
+ GtkWidget *row;
+ GHashTableIter iter;
+ const gchar *id;
+
+ if (info->default_input_source_row)
+ gtk_list_box_append (self->input_sources_listbox, GTK_WIDGET (info->default_input_source_row));
+
+ g_hash_table_iter_init (&iter, info->layout_rows_by_id);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &row))
+ gtk_list_box_append (self->input_sources_listbox, row);
+
+ g_hash_table_iter_init (&iter, info->engine_rows_by_id);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &row))
+ gtk_list_box_append (self->input_sources_listbox, row);
+}
+
+static void
+show_input_sources_for_locale (CcInputChooser *self,
+ LocaleInfo *info)
+{
+ remove_all_rows (self->input_sources_listbox);
+
+ if (!info->back_row)
+ {
+ info->back_row = g_object_ref_sink (back_row_new (info->name));
+ gtk_widget_show (GTK_WIDGET (info->back_row));
+ g_object_set_data (G_OBJECT (info->back_row), "back", GINT_TO_POINTER (TRUE));
+ g_object_set_data (G_OBJECT (info->back_row), "locale-info", info);
+ }
+ gtk_list_box_append (self->input_sources_listbox, GTK_WIDGET (info->back_row));
+
+ add_input_source_rows_for_locale (self, info);
+
+ gtk_adjustment_set_value (self->scroll_adjustment,
+ gtk_adjustment_get_lower (self->scroll_adjustment));
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_selection_mode (self->input_sources_listbox, GTK_SELECTION_SINGLE);
+ gtk_list_box_set_activate_on_single_click (self->input_sources_listbox, FALSE);
+ gtk_list_box_unselect_all (self->input_sources_listbox);
+
+ if (gtk_widget_is_visible (GTK_WIDGET (self->filter_entry)) &&
+ !gtk_widget_is_focus (GTK_WIDGET (self->filter_entry)))
+ gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
+}
+
+static gboolean
+is_current_locale (const gchar *locale)
+{
+ return g_strcmp0 (setlocale (LC_CTYPE, NULL), locale) == 0;
+}
+
+static void
+show_locale_rows (CcInputChooser *self)
+{
+ g_autoptr(GHashTable) initial = NULL;
+ LocaleInfo *info;
+ GHashTableIter iter;
+
+ remove_all_rows (self->input_sources_listbox);
+
+ if (!self->showing_extra)
+ initial = cc_common_language_get_initial_languages ();
+
+ g_hash_table_iter_init (&iter, self->locales);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &info))
+ {
+ if (!info->default_input_source_row &&
+ !g_hash_table_size (info->layout_rows_by_id) &&
+ !g_hash_table_size (info->engine_rows_by_id))
+ continue;
+
+ if (!info->locale_row)
+ {
+ info->locale_row = g_object_ref_sink (locale_row_new (info->name));
+ gtk_widget_show (GTK_WIDGET (info->locale_row));
+ g_object_set_data (G_OBJECT (info->locale_row), "locale-info", info);
+
+ if (!self->showing_extra &&
+ !g_hash_table_contains (initial, info->id) &&
+ !is_current_locale (info->id))
+ g_object_set_data (G_OBJECT (info->locale_row), "is-extra", GINT_TO_POINTER (TRUE));
+ }
+ gtk_list_box_append (self->input_sources_listbox, GTK_WIDGET (info->locale_row));
+ }
+
+ gtk_list_box_append (self->input_sources_listbox, GTK_WIDGET (self->more_row));
+
+ gtk_adjustment_set_value (self->scroll_adjustment,
+ gtk_adjustment_get_lower (self->scroll_adjustment));
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_selection_mode (self->input_sources_listbox, GTK_SELECTION_NONE);
+ gtk_list_box_set_activate_on_single_click (self->input_sources_listbox, TRUE);
+
+ if (gtk_widget_is_visible (GTK_WIDGET (self->filter_entry)) &&
+ !gtk_widget_is_focus (GTK_WIDGET (self->filter_entry)))
+ gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
+}
+
+static gint
+list_sort (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer data)
+{
+ CcInputChooser *self = data;
+ LocaleInfo *ia;
+ LocaleInfo *ib;
+ const gchar *la;
+ const gchar *lb;
+ gint retval;
+
+ /* Always goes at the end */
+ if (a == self->more_row)
+ return 1;
+ if (b == self->more_row)
+ return -1;
+
+ ia = g_object_get_data (G_OBJECT (a), "locale-info");
+ ib = g_object_get_data (G_OBJECT (b), "locale-info");
+
+ /* The "Other" locale always goes at the end */
+ if (!ia->id[0] && ib->id[0])
+ return 1;
+ else if (ia->id[0] && !ib->id[0])
+ return -1;
+
+ retval = g_strcmp0 (ia->name, ib->name);
+ if (retval)
+ return retval;
+
+ la = g_object_get_data (G_OBJECT (a), "name");
+ lb = g_object_get_data (G_OBJECT (b), "name");
+
+ /* Only input sources have a "name" property and they should always
+ go after their respective heading */
+ if (la && !lb)
+ return 1;
+ else if (!la && lb)
+ return -1;
+ else if (!la && !lb)
+ return 0; /* Shouldn't happen */
+
+ /* The default input source always goes first in its group */
+ if (g_object_get_data (G_OBJECT (a), "default"))
+ return -1;
+ if (g_object_get_data (G_OBJECT (b), "default"))
+ return 1;
+
+ return g_strcmp0 (la, lb);
+}
+
+static gboolean
+match_all (gchar **words,
+ const gchar *str)
+{
+ gchar **w;
+
+ for (w = words; *w; ++w)
+ if (!strstr (str, *w))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+match_default_source_in_table (gchar **words,
+ GtkListBoxRow *row)
+{
+ const gchar *source_name;
+ source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
+ if (source_name && match_all (words, source_name))
+ return TRUE;
+ return FALSE;
+}
+
+static gboolean
+match_source_in_table (gchar **words,
+ GHashTable *table)
+{
+ GHashTableIter iter;
+ gpointer row;
+ const gchar *source_name;
+
+ g_hash_table_iter_init (&iter, table);
+ while (g_hash_table_iter_next (&iter, NULL, &row))
+ {
+ source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
+ if (source_name && match_all (words, source_name))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean
+list_filter (GtkListBoxRow *row,
+ gpointer user_data)
+{
+ CcInputChooser *self = user_data;
+ LocaleInfo *info;
+ gboolean is_extra;
+ const gchar *source_name;
+
+ if (row == self->more_row)
+ return !self->showing_extra;
+
+ is_extra = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "is-extra"));
+
+ if (!self->showing_extra && is_extra)
+ return FALSE;
+
+ if (!self->filter_words)
+ return TRUE;
+
+ info = g_object_get_data (G_OBJECT (row), "locale-info");
+
+ if (row == info->back_row)
+ return TRUE;
+
+ if (match_all (self->filter_words, info->unaccented_name))
+ return TRUE;
+
+ if (match_all (self->filter_words, info->untranslated_name))
+ return TRUE;
+
+ source_name = g_object_get_data (G_OBJECT (row), "unaccented-name");
+ if (source_name)
+ {
+ if (match_all (self->filter_words, source_name))
+ return TRUE;
+ }
+ else
+ {
+ if (info->default_input_source_row &&
+ match_default_source_in_table (self->filter_words, info->default_input_source_row))
+ {
+ return TRUE;
+ }
+ if (match_source_in_table (self->filter_words, info->layout_rows_by_id))
+ return TRUE;
+ if (match_source_in_table (self->filter_words, info->engine_rows_by_id))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+strvs_differ (gchar **av,
+ gchar **bv)
+{
+ gchar **a, **b;
+
+ for (a = av, b = bv; *a && *b; ++a, ++b)
+ if (!g_str_equal (*a, *b))
+ return TRUE;
+
+ if (*a == NULL && *b == NULL)
+ return FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+do_filter (CcInputChooser *self)
+{
+ g_auto(GStrv) previous_words = NULL;
+ g_autofree gchar *filter_contents = NULL;
+
+ self->filter_timeout_id = 0;
+
+ filter_contents =
+ cc_util_normalize_casefold_and_unaccent (gtk_editable_get_text (GTK_EDITABLE (self->filter_entry)));
+
+ previous_words = self->filter_words;
+ self->filter_words = g_strsplit_set (g_strstrip (filter_contents), " ", 0);
+
+ if (!self->filter_words[0])
+ {
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_placeholder (self->input_sources_listbox, NULL);
+ }
+ else if (previous_words == NULL || strvs_differ (self->filter_words, previous_words))
+ {
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+ gtk_list_box_set_placeholder (self->input_sources_listbox, self->no_results);
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+on_filter_entry_search_changed_cb (CcInputChooser *self)
+{
+ if (self->filter_timeout_id == 0)
+ self->filter_timeout_id = g_timeout_add (FILTER_TIMEOUT, (GSourceFunc) do_filter, self);
+}
+
+static void
+show_more (CcInputChooser *self)
+{
+ gtk_widget_show (GTK_WIDGET (self->filter_entry));
+ gtk_widget_grab_focus (GTK_WIDGET (self->filter_entry));
+
+ self->showing_extra = TRUE;
+
+ gtk_list_box_invalidate_filter (self->input_sources_listbox);
+}
+
+static void
+on_input_sources_listbox_row_activated_cb (CcInputChooser *self, GtkListBoxRow *row)
+{
+ gpointer data;
+
+ if (!row)
+ return;
+
+ if (row == self->more_row)
+ {
+ show_more (self);
+ return;
+ }
+
+ data = g_object_get_data (G_OBJECT (row), "back");
+ if (data)
+ {
+ show_locale_rows (self);
+ return;
+ }
+
+ data = g_object_get_data (G_OBJECT (row), "name");
+ if (data)
+ {
+ if (gtk_widget_is_sensitive (GTK_WIDGET (self->add_button)))
+ gtk_dialog_response (GTK_DIALOG (self),
+ gtk_dialog_get_response_for_widget (GTK_DIALOG (self),
+ GTK_WIDGET (self->add_button)));
+ return;
+ }
+
+ data = g_object_get_data (G_OBJECT (row), "locale-info");
+ if (data)
+ {
+ show_input_sources_for_locale (self, (LocaleInfo *) data);
+ return;
+ }
+}
+
+static void
+on_input_sources_listbox_selected_rows_changed_cb (CcInputChooser *self)
+{
+ gboolean sensitive = TRUE;
+ GtkListBoxRow *row;
+
+ row = gtk_list_box_get_selected_row (self->input_sources_listbox);
+ if (!row || g_object_get_data (G_OBJECT (row), "back"))
+ sensitive = FALSE;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), sensitive);
+}
+
+static void
+add_default_row (CcInputChooser *self,
+ LocaleInfo *info,
+ const gchar *type,
+ const gchar *id)
+{
+ info->default_input_source_row = input_source_row_new (self, type, id);
+ if (info->default_input_source_row)
+ {
+ gtk_widget_show (GTK_WIDGET (info->default_input_source_row));
+ g_object_ref_sink (GTK_WIDGET (info->default_input_source_row));
+ g_object_set_data (G_OBJECT (info->default_input_source_row), "default", GINT_TO_POINTER (TRUE));
+ g_object_set_data (G_OBJECT (info->default_input_source_row), "locale-info", info);
+ }
+}
+
+static void
+add_rows_to_table (CcInputChooser *self,
+ LocaleInfo *info,
+ GList *list,
+ const gchar *type,
+ const gchar *default_id)
+{
+ GHashTable *table;
+ GtkListBoxRow *row;
+ const gchar *id;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
+ table = info->layout_rows_by_id;
+ else if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS))
+ table = info->engine_rows_by_id;
+ else
+ return;
+
+ while (list)
+ {
+ id = (const gchar *) list->data;
+
+ /* The widget for the default input source lives elsewhere */
+ if (g_strcmp0 (id, default_id))
+ {
+ row = input_source_row_new (self, type, id);
+ gtk_widget_show (GTK_WIDGET (row));
+ if (row)
+ {
+ g_object_set_data (G_OBJECT (row), "locale-info", info);
+ g_hash_table_replace (table, (gpointer) id, g_object_ref_sink (row));
+ }
+ }
+ list = list->next;
+ }
+}
+
+static void
+add_row (CcInputChooser *self,
+ LocaleInfo *info,
+ const gchar *type,
+ const gchar *id)
+{
+ GList tmp = { 0 };
+ tmp.data = (gpointer) id;
+ add_rows_to_table (self, info, &tmp, type, NULL);
+}
+
+static void
+add_row_other (CcInputChooser *self,
+ const gchar *type,
+ const gchar *id)
+{
+ LocaleInfo *info = g_hash_table_lookup (self->locales, "");
+ add_row (self, info, type, id);
+}
+
+#ifdef HAVE_IBUS
+static gboolean
+maybe_set_as_default (CcInputChooser *self,
+ LocaleInfo *info,
+ const gchar *engine_id)
+{
+ const gchar *type, *id;
+
+ if (!gnome_get_input_source_from_locale (info->id, &type, &id))
+ return FALSE;
+
+ if (g_str_equal (type, INPUT_SOURCE_TYPE_IBUS) &&
+ g_str_equal (id, engine_id) &&
+ info->default_input_source_row == NULL)
+ {
+ add_default_row (self, info, type, id);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+get_ibus_locale_infos (CcInputChooser *self)
+{
+ GHashTableIter iter;
+ LocaleInfo *info;
+ const gchar *engine_id;
+ IBusEngineDesc *engine;
+
+ if (!self->ibus_engines || self->is_login)
+ return;
+
+ g_hash_table_iter_init (&iter, self->ibus_engines);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &engine_id, (gpointer *) &engine))
+ {
+ g_autofree gchar *lang_code = NULL;
+ g_autofree gchar *country_code = NULL;
+ const gchar *ibus_locale = ibus_engine_desc_get_language (engine);
+
+ if (gnome_parse_locale (ibus_locale, &lang_code, &country_code, NULL, NULL) &&
+ lang_code != NULL &&
+ country_code != NULL)
+ {
+ g_autofree gchar *locale = g_strdup_printf ("%s_%s.UTF-8", lang_code, country_code);
+
+ info = g_hash_table_lookup (self->locales, locale);
+ if (info)
+ {
+ const gchar *type, *id;
+
+ if (gnome_get_input_source_from_locale (locale, &type, &id) &&
+ g_str_equal (type, INPUT_SOURCE_TYPE_IBUS) &&
+ g_str_equal (id, engine_id))
+ {
+ add_default_row (self, info, type, id);
+ }
+ else
+ {
+ add_row (self, info, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+ else
+ {
+ add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+ else if (lang_code != NULL)
+ {
+ GHashTableIter iter;
+ GHashTable *locales_for_language;
+ g_autofree gchar *language = NULL;
+
+ /* Most IBus engines only specify the language so we try to
+ add them to all locales for that language. */
+
+ language = gnome_get_language_from_code (lang_code, NULL);
+ if (language)
+ locales_for_language = g_hash_table_lookup (self->locales_by_language, language);
+ else
+ locales_for_language = NULL;
+
+ if (locales_for_language)
+ {
+ g_hash_table_iter_init (&iter, locales_for_language);
+ while (g_hash_table_iter_next (&iter, (gpointer *) &info, NULL))
+ if (!maybe_set_as_default (self, info, engine_id))
+ add_row (self, info, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ else
+ {
+ add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+ else
+ {
+ add_row_other (self, INPUT_SOURCE_TYPE_IBUS, engine_id);
+ }
+ }
+}
+#endif /* HAVE_IBUS */
+
+static void
+add_locale_to_table (GHashTable *table,
+ const gchar *lang_code,
+ LocaleInfo *info)
+{
+ GHashTable *set;
+ g_autofree gchar *language = NULL;
+
+ language = gnome_get_language_from_code (lang_code, NULL);
+
+ set = g_hash_table_lookup (table, language);
+ if (!set)
+ {
+ set = g_hash_table_new (NULL, NULL);
+ g_hash_table_replace (table, g_strdup (language), set);
+ }
+ g_hash_table_add (set, info);
+}
+
+static void
+add_ids_to_set (GHashTable *set,
+ GList *list)
+{
+ while (list)
+ {
+ g_hash_table_add (set, list->data);
+ list = list->next;
+ }
+}
+
+static void
+get_locale_infos (CcInputChooser *self)
+{
+ g_autoptr(GHashTable) layouts_with_locale = NULL;
+ LocaleInfo *info;
+ g_auto(GStrv) locale_ids = NULL;
+ gchar **locale;
+ g_autoptr(GList) all_layouts = NULL;
+ GList *l;
+
+ self->locales = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, locale_info_free);
+ self->locales_by_language = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, (GDestroyNotify) g_hash_table_unref);
+
+ layouts_with_locale = g_hash_table_new (g_str_hash, g_str_equal);
+
+ locale_ids = gnome_get_all_locales ();
+ for (locale = locale_ids; *locale; ++locale)
+ {
+ g_autofree gchar *lang_code = NULL;
+ g_autofree gchar *country_code = NULL;
+ g_autofree gchar *simple_locale = NULL;
+ g_autofree gchar *tmp = NULL;
+ const gchar *type = NULL;
+ const gchar *id = NULL;
+ g_autoptr(GList) language_layouts = NULL;
+
+ if (!gnome_parse_locale (*locale, &lang_code, &country_code, NULL, NULL))
+ continue;
+
+ if (country_code != NULL)
+ simple_locale = g_strdup_printf ("%s_%s.UTF-8", lang_code, country_code);
+ else
+ simple_locale = g_strdup_printf ("%s.UTF-8", lang_code);
+
+ if (g_hash_table_contains (self->locales, simple_locale))
+ continue;
+
+ info = g_new0 (LocaleInfo, 1);
+ info->id = g_strdup (simple_locale);
+ info->name = gnome_get_language_from_locale (simple_locale, NULL);
+ info->unaccented_name = cc_util_normalize_casefold_and_unaccent (info->name);
+ tmp = gnome_get_language_from_locale (simple_locale, "C");
+ info->untranslated_name = cc_util_normalize_casefold_and_unaccent (tmp);
+
+ g_hash_table_replace (self->locales, g_strdup (simple_locale), info);
+ add_locale_to_table (self->locales_by_language, lang_code, info);
+
+ if (gnome_get_input_source_from_locale (simple_locale, &type, &id) &&
+ g_str_equal (type, INPUT_SOURCE_TYPE_XKB))
+ {
+ add_default_row (self, info, type, id);
+ g_hash_table_add (layouts_with_locale, (gpointer) id);
+ }
+
+ /* We don't own these ids */
+ info->layout_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+ info->engine_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+
+ language_layouts = gnome_xkb_info_get_layouts_for_language (self->xkb_info, lang_code);
+ add_rows_to_table (self, info, language_layouts, INPUT_SOURCE_TYPE_XKB, id);
+ add_ids_to_set (layouts_with_locale, language_layouts);
+
+ if (country_code != NULL)
+ {
+ g_autoptr(GList) country_layouts = gnome_xkb_info_get_layouts_for_country (self->xkb_info, country_code);
+ add_rows_to_table (self, info, country_layouts, INPUT_SOURCE_TYPE_XKB, id);
+ add_ids_to_set (layouts_with_locale, country_layouts);
+ }
+ }
+
+ /* Add a "Other" locale to hold the remaining input sources */
+ info = g_new0 (LocaleInfo, 1);
+ info->id = g_strdup ("");
+ info->name = g_strdup (C_("Input Source", "Other"));
+ info->unaccented_name = g_strdup ("");
+ info->untranslated_name = g_strdup ("");
+ g_hash_table_replace (self->locales, g_strdup (info->id), info);
+
+ info->layout_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+ info->engine_rows_by_id = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, g_object_unref);
+
+ all_layouts = gnome_xkb_info_get_all_layouts (self->xkb_info);
+ for (l = all_layouts; l; l = l->next)
+ if (!g_hash_table_contains (layouts_with_locale, l->data))
+ add_row_other (self, INPUT_SOURCE_TYPE_XKB, l->data);
+}
+
+/*
+static gboolean
+on_filter_entry_key_release_event_cb (CcInputChooser *self, GdkEventKey *event)
+{
+ if (event->keyval == GDK_KEY_Escape) {
+ self->showing_extra = FALSE;
+ gtk_entry_set_text (GTK_ENTRY (self->filter_entry), "");
+ gtk_widget_hide (GTK_WIDGET (self->filter_entry));
+ g_clear_pointer (&self->filter_words, g_strfreev);
+ show_locale_rows (self);
+ }
+
+ return FALSE;
+}
+ */
+
+static void
+cc_input_chooser_dispose (GObject *object)
+{
+ CcInputChooser *self = CC_INPUT_CHOOSER (object);
+
+ g_clear_object (&self->more_row);
+ g_clear_object (&self->no_results);
+ g_clear_object (&self->xkb_info);
+ g_clear_pointer (&self->ibus_engines, g_hash_table_unref);
+ g_clear_pointer (&self->locales, g_hash_table_unref);
+ g_clear_pointer (&self->locales_by_language, g_hash_table_unref);
+ g_clear_pointer (&self->filter_words, g_strfreev);
+ g_clear_handle_id (&self->filter_timeout_id, g_source_remove);
+
+ G_OBJECT_CLASS (cc_input_chooser_parent_class)->dispose (object);
+}
+
+void
+cc_input_chooser_class_init (CcInputChooserClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_input_chooser_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-input-chooser.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, add_button);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, filter_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, input_sources_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, login_label);
+ gtk_widget_class_bind_template_child (widget_class, CcInputChooser, scroll_adjustment);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_input_sources_listbox_selected_rows_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, on_filter_entry_search_changed_cb);
+ //gtk_widget_class_bind_template_callback (widget_class, on_filter_entry_key_release_event_cb);
+}
+
+void
+cc_input_chooser_init (CcInputChooser *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_search_entry_set_key_capture_widget (self->filter_entry, GTK_WIDGET (self));
+}
+
+CcInputChooser *
+cc_input_chooser_new (gboolean is_login,
+ GnomeXkbInfo *xkb_info,
+ GHashTable *ibus_engines)
+{
+ CcInputChooser *self;
+
+ self = g_object_new (CC_TYPE_INPUT_CHOOSER,
+ "use-header-bar", 1,
+ NULL);
+
+ self->is_login = is_login;
+ self->xkb_info = g_object_ref (xkb_info);
+ if (ibus_engines)
+ self->ibus_engines = g_hash_table_ref (ibus_engines);
+
+ self->more_row = g_object_ref_sink (more_row_new ());
+ gtk_widget_show (GTK_WIDGET (self->more_row));
+ self->no_results = g_object_ref_sink (no_results_widget_new ());
+ gtk_widget_show (self->no_results);
+
+ gtk_list_box_set_filter_func (self->input_sources_listbox, list_filter, self, NULL);
+ gtk_list_box_set_sort_func (self->input_sources_listbox, list_sort, self, NULL);
+
+ if (self->is_login)
+ gtk_widget_show (GTK_WIDGET (self->login_label));
+
+ get_locale_infos (self);
+#ifdef HAVE_IBUS
+ get_ibus_locale_infos (self);
+#endif /* HAVE_IBUS */
+ show_locale_rows (self);
+
+ return self;
+}
+
+void
+cc_input_chooser_set_ibus_engines (CcInputChooser *self,
+ GHashTable *ibus_engines)
+{
+ g_return_if_fail (CC_IS_INPUT_CHOOSER (self));
+
+#ifdef HAVE_IBUS
+ /* This should only be called once when IBus shows up in case it
+ wasn't up yet when the user opened the input chooser dialog. */
+ g_return_if_fail (self->ibus_engines == NULL);
+
+ self->ibus_engines = ibus_engines;
+ get_ibus_locale_infos (self);
+ show_locale_rows (self);
+#endif /* HAVE_IBUS */
+}
+
+CcInputSource *
+cc_input_chooser_get_source (CcInputChooser *self)
+{
+ GtkListBoxRow *selected;
+ const gchar *t, *i;
+
+ g_return_val_if_fail (CC_IS_INPUT_CHOOSER (self), FALSE);
+
+ selected = gtk_list_box_get_selected_row (self->input_sources_listbox);
+ if (!selected)
+ return NULL;
+
+ t = g_object_get_data (G_OBJECT (selected), "type");
+ i = g_object_get_data (G_OBJECT (selected), "id");
+
+ if (!t || !i)
+ return FALSE;
+
+ if (g_strcmp0 (t, "xkb") == 0)
+ return CC_INPUT_SOURCE (cc_input_source_xkb_new_from_id (self->xkb_info, i));
+ else if (g_strcmp0 (t, "ibus") == 0)
+ return CC_INPUT_SOURCE (cc_input_source_ibus_new (i));
+ else
+ return NULL;
+}
diff --git a/panels/keyboard/cc-input-chooser.h b/panels/keyboard/cc-input-chooser.h
new file mode 100644
index 0000000..8363782
--- /dev/null
+++ b/panels/keyboard/cc-input-chooser.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2013 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 <gtk/gtk.h>
+
+#include "cc-input-source.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-xkb-info.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_CHOOSER (cc_input_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputChooser, cc_input_chooser, CC, INPUT_CHOOSER, GtkDialog)
+
+CcInputChooser *cc_input_chooser_new (gboolean is_login,
+ GnomeXkbInfo *xkb_info,
+ GHashTable *ibus_engines);
+
+void cc_input_chooser_set_ibus_engines (CcInputChooser *chooser,
+ GHashTable *ibus_engines);
+
+CcInputSource *cc_input_chooser_get_source (CcInputChooser *chooser);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-input-chooser.ui b/panels/keyboard/cc-input-chooser.ui
new file mode 100644
index 0000000..74dd39a
--- /dev/null
+++ b/panels/keyboard/cc-input-chooser.ui
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcInputChooser" parent="GtkDialog">
+ <property name="title" translatable="yes">Add an Input Source</property>
+ <property name="modal">True</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="resizable">True</property>
+ <child type="action">
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="add_button">
+ <property name="label" translatable="yes">_Add</property>
+ <property name="sensitive">False</property>
+ <property name="use_underline">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">0</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="vexpand">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="min-content-height">300</property>
+ <property name="vadjustment">scroll_adjustment</property>
+ <property name="child">
+ <object class="GtkViewport">
+ <property name="scroll-to-focus">True</property>
+ <property name="child">
+ <object class="AdwClamp">
+ <property name="child">
+ <object class="GtkListBox" id="input_sources_listbox">
+ <property name="halign">fill</property>
+ <property name="valign">start</property>
+ <property name="margin-top">24</property>
+ <property name="margin-bottom">24</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <signal name="row-activated" handler="on_input_sources_listbox_row_activated_cb" object="CcInputChooser" swapped="yes" />
+ <signal name="selected-rows-changed" handler="on_input_sources_listbox_selected_rows_changed_cb" object="CcInputChooser" swapped="yes" />
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </property>
+ </object>
+ </property>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible" bind-source="filter_entry" bind-property="visible" bind-flags="sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSearchEntry" id="filter_entry">
+ <property name="visible">False</property>
+ <property name="hexpand">True</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="margin-top">6</property>
+ <property name="margin-bottom">6</property>
+ <accessibility>
+ <property name="label" translatable="yes">Search</property>
+ </accessibility>
+ <signal name="search-changed" handler="on_filter_entry_search_changed_cb" object="CcInputChooser" swapped="yes" />
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="login_label">
+ <property name="visible">False</property>
+ <property name="wrap">True</property>
+ <property name="label" translatable="yes">Input methods can’t be used on the login screen</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <property name="margin-bottom">6</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="-5" default="true">add_button</action-widget>
+ <action-widget response="-6">cancel_button</action-widget>
+ </action-widgets>
+ </template>
+ <object class="GtkAdjustment" id="scroll_adjustment">
+ </object>
+</interface>
diff --git a/panels/keyboard/cc-input-list-box.c b/panels/keyboard/cc-input-list-box.c
new file mode 100644
index 0000000..5ad9f15
--- /dev/null
+++ b/panels/keyboard/cc-input-list-box.c
@@ -0,0 +1,821 @@
+/* cc-input-list-box.c
+ *
+ * Copyright (C) 2010 Intel, Inc
+ * Copyright (C) 2020 System76, 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: Sergey Udaltsov <svu@gnome.org>
+ * Ian Douglas Scott <idscott@system76.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-xkb-info.h>
+
+#include "cc-input-list-box.h"
+#include "cc-input-chooser.h"
+#include "cc-input-row.h"
+#include "cc-input-source-ibus.h"
+#include "cc-input-source-xkb.h"
+
+#ifdef HAVE_IBUS
+#include <ibus.h>
+#endif
+
+#define GNOME_DESKTOP_INPUT_SOURCES_DIR "org.gnome.desktop.input-sources"
+#define KEY_INPUT_SOURCES "sources"
+
+struct _CcInputListBox {
+ AdwBin parent_instance;
+
+ GtkListBoxRow *add_input_row;
+ GtkListBox *listbox;
+ GtkListBoxRow *no_inputs_row;
+
+ GCancellable *cancellable;
+
+ gboolean login;
+ gboolean login_auto_apply;
+ GPermission *permission;
+ GDBusProxy *localed;
+
+ GSettings *input_settings;
+ GnomeXkbInfo *xkb_info;
+#ifdef HAVE_IBUS
+ IBusBus *ibus;
+ GHashTable *ibus_engines;
+#endif
+};
+
+G_DEFINE_TYPE (CcInputListBox, cc_input_list_box, ADW_TYPE_BIN)
+
+typedef struct
+{
+ CcInputListBox *panel;
+ CcInputRow *source;
+ CcInputRow *dest;
+} RowData;
+
+static RowData *
+row_data_new (CcInputListBox *panel, CcInputRow *source, CcInputRow *dest)
+{
+ RowData *data = g_malloc0 (sizeof (RowData));
+ data->panel = panel;
+ data->source = g_object_ref (source);
+ if (dest != NULL)
+ data->dest = g_object_ref (dest);
+ return data;
+}
+
+static void
+row_data_free (RowData *data)
+{
+ g_clear_object (&data->source);
+ g_clear_object (&data->dest);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (RowData, row_data_free)
+
+static void show_input_chooser (CcInputListBox *self);
+
+#ifdef HAVE_IBUS
+static void
+update_ibus_active_sources (CcInputListBox *self)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ CcInputRow *row;
+ CcInputSourceIBus *source;
+ IBusEngineDesc *engine_desc;
+
+ if (!CC_IS_INPUT_ROW (child))
+ continue;
+ row = CC_INPUT_ROW (child);
+
+ if (!CC_IS_INPUT_SOURCE_IBUS (cc_input_row_get_source (row)))
+ continue;
+ source = CC_INPUT_SOURCE_IBUS (cc_input_row_get_source (row));
+
+ engine_desc = g_hash_table_lookup (self->ibus_engines, cc_input_source_ibus_get_engine_name (source));
+ if (engine_desc != NULL)
+ cc_input_source_ibus_set_engine_desc (source, engine_desc);
+ }
+}
+
+static void
+fetch_ibus_engines_result (GObject *object,
+ GAsyncResult *result,
+ CcInputListBox *self)
+{
+ g_autoptr(GList) list = NULL;
+ GList *l;
+ g_autoptr(GError) error = NULL;
+
+ list = ibus_bus_list_engines_async_finish (IBUS_BUS (object), result, &error);
+ if (!list && error) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Couldn't finish IBus request: %s", error->message);
+ return;
+ }
+
+ /* Maps engine ids to engine description objects */
+ self->ibus_engines = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
+
+ for (l = list; l; l = l->next) {
+ IBusEngineDesc *engine = l->data;
+ const gchar *engine_id = ibus_engine_desc_get_name (engine);
+
+ if (g_str_has_prefix (engine_id, "xkb:"))
+ g_object_unref (engine);
+ else
+ g_hash_table_replace (self->ibus_engines, (gpointer)engine_id, engine);
+ }
+
+ update_ibus_active_sources (self);
+}
+
+static void
+fetch_ibus_engines (CcInputListBox *self)
+{
+ ibus_bus_list_engines_async (self->ibus,
+ -1,
+ self->cancellable,
+ (GAsyncReadyCallback)fetch_ibus_engines_result,
+ self);
+
+ /* We've got everything we needed, don't want to be called again. */
+ g_signal_handlers_disconnect_by_func (self->ibus, fetch_ibus_engines, self);
+}
+
+static void
+maybe_start_ibus (void)
+{
+ /* IBus doesn't export API in the session bus. The only thing
+ * we have there is a well known name which we can use as a
+ * sure-fire way to activate it.
+ */
+ g_bus_unwatch_name (g_bus_watch_name (G_BUS_TYPE_SESSION,
+ IBUS_SERVICE_IBUS,
+ G_BUS_NAME_WATCHER_FLAGS_AUTO_START,
+ NULL,
+ NULL,
+ NULL,
+ NULL));
+}
+
+#endif
+
+static gboolean
+keynav_failed_cb (CcInputListBox *self,
+ GtkDirectionType direction,
+ GtkWidget *list)
+{
+ GtkWidget *toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (self)));
+
+ if (!toplevel)
+ return FALSE;
+
+ if (direction != GTK_DIR_UP && direction != GTK_DIR_DOWN)
+ return FALSE;
+
+ return gtk_widget_child_focus (toplevel, direction == GTK_DIR_UP ?
+ GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD);
+}
+
+static void
+row_settings_cb (CcInputListBox *self,
+ CcInputRow *row)
+{
+ CcInputSourceIBus *source;
+ g_autoptr(GdkAppLaunchContext) ctx = NULL;
+ GDesktopAppInfo *app_info;
+ g_autoptr(GError) error = NULL;
+
+ g_return_if_fail (CC_IS_INPUT_SOURCE_IBUS (cc_input_row_get_source (row)));
+ source = CC_INPUT_SOURCE_IBUS (cc_input_row_get_source (row));
+
+ app_info = cc_input_source_ibus_get_app_info (source);
+ if (app_info == NULL)
+ return;
+
+ ctx = gdk_display_get_app_launch_context (gdk_display_get_default ());
+ gdk_app_launch_context_set_timestamp (ctx, GDK_CURRENT_TIME);
+
+ g_app_launch_context_setenv (G_APP_LAUNCH_CONTEXT (ctx),
+ "IBUS_ENGINE_NAME", cc_input_source_ibus_get_engine_name (source));
+
+ if (!g_app_info_launch (G_APP_INFO (app_info), NULL, G_APP_LAUNCH_CONTEXT (ctx), &error))
+ g_warning ("Failed to launch input source setup: %s", error->message);
+}
+
+static void
+row_layout_cb (CcInputListBox *self,
+ CcInputRow *row)
+{
+ CcInputSource *source;
+ const gchar *layout, *layout_variant;
+ g_autofree gchar *commandline = NULL;
+
+ source = cc_input_row_get_source (row);
+
+ layout = cc_input_source_get_layout (source);
+ layout_variant = cc_input_source_get_layout_variant (source);
+
+ if (layout_variant && layout_variant[0])
+ commandline = g_strdup_printf ("gkbd-keyboard-display -l \"%s\t%s\"",
+ layout, layout_variant);
+ else
+ commandline = g_strdup_printf ("gkbd-keyboard-display -l %s",
+ layout);
+
+ g_spawn_command_line_async (commandline, NULL);
+}
+
+static void move_input (CcInputListBox *self, CcInputRow *source, CcInputRow *dest);
+
+static void
+row_moved_cb (CcInputListBox *self,
+ CcInputRow *dest_row,
+ CcInputRow *row)
+{
+ move_input (self, row, dest_row);
+}
+
+static void remove_input (CcInputListBox *self, CcInputRow *row);
+
+static void
+row_removed_cb (CcInputListBox *self,
+ CcInputRow *row)
+{
+ remove_input (self, row);
+}
+
+static void
+update_input_rows (CcInputListBox *self)
+{
+ GtkWidget *child;
+ guint n_input_rows = 0;
+
+ child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ while ((child = gtk_widget_get_next_sibling (child)) != NULL)
+ if (CC_IS_INPUT_ROW (child))
+ n_input_rows++;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ CcInputRow *row;
+
+ if (!CC_IS_INPUT_ROW (child))
+ continue;
+ row = CC_INPUT_ROW (child);
+
+ cc_input_row_set_removable (row, n_input_rows > 1);
+ cc_input_row_set_draggable (row, n_input_rows > 1);
+ }
+}
+
+static void
+add_input_row (CcInputListBox *self, CcInputSource *source)
+{
+ CcInputRow *row;
+
+ gtk_widget_set_visible (GTK_WIDGET (self->no_inputs_row), FALSE);
+
+ row = cc_input_row_new (source);
+ gtk_widget_show (GTK_WIDGET (row));
+ g_signal_connect_object (row, "show-settings", G_CALLBACK (row_settings_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (row, "show-layout", G_CALLBACK (row_layout_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (row, "move-row", G_CALLBACK (row_moved_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (row, "remove-row", G_CALLBACK (row_removed_cb), self, G_CONNECT_SWAPPED);
+ gtk_list_box_insert (self->listbox, GTK_WIDGET (row), gtk_list_box_row_get_index (self->add_input_row));
+ update_input_rows (self);
+}
+
+static void
+add_input_sources (CcInputListBox *self,
+ GVariant *sources)
+{
+ GVariantIter iter;
+ const gchar *type, *id;
+
+ if (g_variant_n_children (sources) < 1) {
+ gtk_widget_set_visible (GTK_WIDGET (self->no_inputs_row), TRUE);
+ return;
+ }
+
+ g_variant_iter_init (&iter, sources);
+ while (g_variant_iter_next (&iter, "(&s&s)", &type, &id)) {
+ g_autoptr(CcInputSource) source = NULL;
+
+ if (g_str_equal (type, "xkb")) {
+ source = CC_INPUT_SOURCE (cc_input_source_xkb_new_from_id (self->xkb_info, id));
+ } else if (g_str_equal (type, "ibus")) {
+ source = CC_INPUT_SOURCE (cc_input_source_ibus_new (id));
+#ifdef HAVE_IBUS
+ if (self->ibus_engines) {
+ IBusEngineDesc *engine_desc = g_hash_table_lookup (self->ibus_engines, id);
+ if (engine_desc != NULL)
+ cc_input_source_ibus_set_engine_desc (CC_INPUT_SOURCE_IBUS (source), engine_desc);
+ }
+#endif
+ } else {
+ g_warning ("Unhandled input source type '%s'", type);
+ continue;
+ }
+
+ add_input_row (self, source);
+ }
+}
+
+static void
+add_input_sources_from_settings (CcInputListBox *self)
+{
+ g_autoptr(GVariant) sources = NULL;
+ sources = g_settings_get_value (self->input_settings, "sources");
+ add_input_sources (self, sources);
+}
+
+static void
+clear_input_sources (CcInputListBox *self)
+{
+ GtkWidget *child;
+
+ child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ while (child) {
+ GtkWidget *next = gtk_widget_get_next_sibling (child);
+
+ if (CC_IS_INPUT_ROW (child))
+ gtk_list_box_remove (self->listbox, GTK_WIDGET (child));
+
+ child = next;
+ }
+}
+
+static CcInputRow *
+get_row_by_source (CcInputListBox *self, CcInputSource *source)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ CcInputRow *row;
+
+ if (!CC_IS_INPUT_ROW (child))
+ continue;
+ row = CC_INPUT_ROW (child);
+
+ if (cc_input_source_matches (source, cc_input_row_get_source (row)))
+ return row;
+ }
+
+ return NULL;
+}
+
+static void
+input_sources_changed (CcInputListBox *self,
+ const gchar *key)
+{
+ CcInputRow *selected;
+ g_autoptr(CcInputSource) source = NULL;
+
+ selected = CC_INPUT_ROW (gtk_list_box_get_selected_row (self->listbox));
+ if (selected)
+ source = g_object_ref (cc_input_row_get_source (selected));
+ clear_input_sources (self);
+ add_input_sources_from_settings (self);
+ if (source != NULL) {
+ CcInputRow *row = get_row_by_source (self, source);
+ if (row != NULL)
+ gtk_list_box_select_row (self->listbox, GTK_LIST_BOX_ROW (row));
+ }
+}
+
+static void
+set_input_settings (CcInputListBox *self)
+{
+ GVariantBuilder builder;
+ GtkWidget *child;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ss)"));
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ CcInputRow *row;
+ CcInputSource *source;
+
+ if (!CC_IS_INPUT_ROW (child))
+ continue;
+ row = CC_INPUT_ROW (child);
+ source = cc_input_row_get_source (row);
+
+ if (CC_IS_INPUT_SOURCE_XKB (source)) {
+ g_autofree gchar *id = cc_input_source_xkb_get_id (CC_INPUT_SOURCE_XKB (source));
+ g_variant_builder_add (&builder, "(ss)", "xkb", id);
+ } else if (CC_IS_INPUT_SOURCE_IBUS (source)) {
+ g_variant_builder_add (&builder, "(ss)", "ibus",
+ cc_input_source_ibus_get_engine_name (CC_INPUT_SOURCE_IBUS (source)));
+ }
+ }
+
+ g_settings_set_value (self->input_settings, KEY_INPUT_SOURCES, g_variant_builder_end (&builder));
+}
+
+static void set_localed_input (CcInputListBox *self);
+
+static void
+update_input (CcInputListBox *self)
+{
+ if (self->login) {
+ set_localed_input (self);
+ } else {
+ set_input_settings (self);
+ if (self->login_auto_apply)
+ set_localed_input (self);
+ }
+}
+
+static void
+on_chooser_response_cb (GtkDialog *dialog,
+ gint response,
+ CcInputListBox *self)
+{
+
+ if (response == GTK_RESPONSE_OK) {
+ CcInputSource *source;
+
+ source = cc_input_chooser_get_source (CC_INPUT_CHOOSER (dialog));
+ if (source != NULL && get_row_by_source (self, source) == NULL) {
+ add_input_row (self, source);
+ update_input (self);
+ }
+ }
+ gtk_window_destroy (GTK_WINDOW (dialog));
+}
+
+static void
+show_input_chooser (CcInputListBox *self)
+{
+ CcInputChooser *chooser;
+
+ chooser = cc_input_chooser_new (self->login,
+ self->xkb_info,
+#ifdef HAVE_IBUS
+ self->ibus_engines
+#else
+ NULL
+#endif
+ );
+ gtk_window_set_transient_for (GTK_WINDOW (chooser),
+ GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))));
+ g_signal_connect (chooser, "response", G_CALLBACK (on_chooser_response_cb), self);
+ gtk_window_present (GTK_WINDOW (chooser));
+}
+
+// Duplicated from cc-region-panel.c
+static gboolean
+permission_acquired (GPermission *permission, GAsyncResult *res, const gchar *action)
+{
+ g_autoptr(GError) error = NULL;
+
+ if (!g_permission_acquire_finish (permission, res, &error)) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to acquire permission to %s: %s\n", error->message, action);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+add_input_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ CcInputListBox *self = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "add input"))
+ show_input_chooser (self);
+}
+
+static void
+add_input (CcInputListBox *self)
+{
+ if (!self->login) {
+ show_input_chooser (self);
+ } else if (g_permission_get_allowed (self->permission)) {
+ show_input_chooser (self);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ self->cancellable,
+ add_input_permission_cb,
+ self);
+ }
+}
+
+static GtkWidget *
+find_sibling (GtkWidget *child)
+{
+ GtkWidget *sibling;
+
+ for (sibling = gtk_widget_get_next_sibling (child);
+ sibling;
+ sibling = gtk_widget_get_next_sibling (child)) {
+ if (gtk_widget_get_visible (sibling) && gtk_widget_get_child_visible (sibling))
+ return sibling;
+ }
+
+ for (sibling = gtk_widget_get_prev_sibling (child);
+ sibling;
+ sibling = gtk_widget_get_prev_sibling (child)) {
+ if (gtk_widget_get_visible (sibling) && gtk_widget_get_child_visible (sibling))
+ return sibling;
+ }
+
+ return NULL;
+}
+
+static void
+do_remove_input (CcInputListBox *self, CcInputRow *row)
+{
+ GtkWidget *sibling;
+
+ sibling = find_sibling (GTK_WIDGET (row));
+ gtk_list_box_remove (self->listbox, GTK_WIDGET (row));
+ gtk_list_box_select_row (self->listbox, GTK_LIST_BOX_ROW (sibling));
+
+ update_input (self);
+ update_input_rows (self);
+}
+
+static void
+remove_input_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ RowData *data = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "remove input"))
+ do_remove_input (data->panel, data->source);
+}
+
+static void
+remove_input (CcInputListBox *self, CcInputRow *row)
+{
+ if (!self->login) {
+ do_remove_input (self, row);
+ } else if (g_permission_get_allowed (self->permission)) {
+ do_remove_input (self, row);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ self->cancellable,
+ remove_input_permission_cb,
+ row_data_new (self, row, NULL));
+ }
+}
+
+static void
+do_move_input (CcInputListBox *self, CcInputRow *source, CcInputRow *dest)
+{
+ gint dest_index;
+
+ dest_index = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (dest));
+
+ g_object_ref (source);
+ gtk_list_box_remove (self->listbox, GTK_WIDGET (source));
+ gtk_list_box_insert (self->listbox, GTK_WIDGET (source), dest_index);
+ g_object_unref (source);
+
+ update_input (self);
+}
+
+static void
+move_input_permission_cb (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+ RowData *data = user_data;
+ if (permission_acquired (G_PERMISSION (source), res, "move input"))
+ do_move_input (data->panel, data->source, data->dest);
+}
+
+static void
+move_input (CcInputListBox *self,
+ CcInputRow *source,
+ CcInputRow *dest)
+{
+ if (!self->login) {
+ do_move_input (self, source, dest);
+ } else if (g_permission_get_allowed (self->permission)) {
+ do_move_input (self, source, dest);
+ } else if (g_permission_get_can_acquire (self->permission)) {
+ g_permission_acquire_async (self->permission,
+ self->cancellable,
+ move_input_permission_cb,
+ row_data_new (self, source, dest));
+ }
+}
+
+static void
+input_row_activated_cb (CcInputListBox *self, GtkListBoxRow *row)
+{
+ if (row == self->add_input_row) {
+ add_input (self);
+ }
+}
+
+static void
+add_input_sources_from_localed (CcInputListBox *self)
+{
+ g_autoptr(GVariant) layout_property = NULL;
+ g_autoptr(GVariant) variant_property = NULL;
+ const gchar *s;
+ g_auto(GStrv) layouts = NULL;
+ g_auto(GStrv) variants = NULL;
+ gint i, n;
+
+ if (!self->localed)
+ return;
+
+ layout_property = g_dbus_proxy_get_cached_property (self->localed, "X11Layout");
+ if (layout_property) {
+ s = g_variant_get_string (layout_property, NULL);
+ layouts = g_strsplit (s, ",", -1);
+ }
+
+ variant_property = g_dbus_proxy_get_cached_property (self->localed, "X11Variant");
+ if (variant_property) {
+ s = g_variant_get_string (variant_property, NULL);
+ if (s && *s)
+ variants = g_strsplit (s, ",", -1);
+ }
+
+ if (variants && variants[0])
+ n = MIN (g_strv_length (layouts), g_strv_length (variants));
+ else if (layouts && layouts[0])
+ n = g_strv_length (layouts);
+ else
+ n = 0;
+
+ for (i = 0; i < n && layouts[i][0]; i++) {
+ const char *variant = variants ? variants[i] : NULL;
+ g_autoptr(CcInputSourceXkb) source = cc_input_source_xkb_new (self->xkb_info, layouts[i], variant);
+ add_input_row (self, CC_INPUT_SOURCE (source));
+ }
+ gtk_widget_set_visible (GTK_WIDGET (self->no_inputs_row), n == 0);
+}
+
+static void
+set_localed_input (CcInputListBox *self)
+{
+ g_autoptr(GString) layouts = NULL;
+ g_autoptr(GString) variants = NULL;
+ GtkWidget *child;
+
+ layouts = g_string_new ("");
+ variants = g_string_new ("");
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ CcInputRow *row;
+ CcInputSourceXkb *source;
+ g_autofree gchar *id = NULL;
+ const gchar *l, *v;
+
+ if (!CC_IS_INPUT_ROW (child))
+ continue;
+ row = CC_INPUT_ROW (child);
+
+ if (!CC_IS_INPUT_SOURCE_XKB (cc_input_row_get_source (row)))
+ continue;
+ source = CC_INPUT_SOURCE_XKB (cc_input_row_get_source (row));
+
+ id = cc_input_source_xkb_get_id (source);
+ if (gnome_xkb_info_get_layout_info (self->xkb_info, id, NULL, NULL, &l, &v)) {
+ if (layouts->str[0]) {
+ g_string_append_c (layouts, ',');
+ g_string_append_c (variants, ',');
+ }
+ g_string_append (layouts, l);
+ g_string_append (variants, v);
+ }
+ }
+
+ g_dbus_proxy_call (self->localed,
+ "SetX11Keyboard",
+ g_variant_new ("(ssssbb)", layouts->str, "", variants->str, "", TRUE, TRUE),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+}
+
+static void
+cc_input_list_box_finalize (GObject *object)
+{
+ CcInputListBox *self = CC_INPUT_LIST_BOX (object);
+
+ g_cancellable_cancel (self->cancellable);
+
+ g_clear_object (&self->input_settings);
+ g_clear_object (&self->xkb_info);
+#ifdef HAVE_IBUS
+ g_clear_object (&self->ibus);
+ g_clear_pointer (&self->ibus_engines, g_hash_table_destroy);
+#endif
+
+ G_OBJECT_CLASS (cc_input_list_box_parent_class)->finalize (object);
+}
+
+static void
+cc_input_list_box_class_init (CcInputListBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = cc_input_list_box_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-input-list-box.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcInputListBox, add_input_row);
+ gtk_widget_class_bind_template_child (widget_class, CcInputListBox, listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcInputListBox, no_inputs_row);
+
+ gtk_widget_class_bind_template_callback (widget_class, input_row_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, keynav_failed_cb);
+}
+
+static void
+cc_input_list_box_init (CcInputListBox *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->login = FALSE;
+ self->login_auto_apply = FALSE;
+ self->localed = NULL;
+ self->permission = NULL;
+
+ self->cancellable = g_cancellable_new();
+
+ self->input_settings = g_settings_new (GNOME_DESKTOP_INPUT_SOURCES_DIR);
+
+ self->xkb_info = gnome_xkb_info_new ();
+
+#ifdef HAVE_IBUS
+ ibus_init ();
+ if (!self->ibus) {
+ self->ibus = ibus_bus_new_async ();
+ if (ibus_bus_is_connected (self->ibus))
+ fetch_ibus_engines (self);
+ else
+ g_signal_connect_object (self->ibus, "connected",
+ G_CALLBACK (fetch_ibus_engines), self,
+ G_CONNECT_SWAPPED);
+ }
+ maybe_start_ibus ();
+#endif
+
+ g_signal_connect_object (self->input_settings, "changed::" KEY_INPUT_SOURCES,
+ G_CALLBACK (input_sources_changed), self, G_CONNECT_SWAPPED);
+
+ add_input_sources_from_settings (self);
+}
+
+void
+cc_input_list_box_set_login (CcInputListBox *self, gboolean login)
+{
+ self->login = login;
+ clear_input_sources (self);
+ if (login)
+ add_input_sources_from_localed (self);
+ else
+ add_input_sources_from_settings (self);
+}
+
+void
+cc_input_list_box_set_login_auto_apply (CcInputListBox *self, gboolean login_auto_apply)
+{
+ self->login_auto_apply = login_auto_apply;
+}
+
+void
+cc_input_list_box_set_localed (CcInputListBox *self, GDBusProxy *localed)
+{
+ self->localed = localed;
+}
+
+void
+cc_input_list_box_set_permission (CcInputListBox *self, GPermission *permission)
+{
+ self->permission = permission;
+}
diff --git a/panels/keyboard/cc-input-list-box.h b/panels/keyboard/cc-input-list-box.h
new file mode 100644
index 0000000..7045053
--- /dev/null
+++ b/panels/keyboard/cc-input-list-box.h
@@ -0,0 +1,43 @@
+/* cc-input-list-box.c
+ *
+ * Copyright (C) 2010 Intel, Inc
+ * Copyright (C) 2020 System76, 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: Sergey Udaltsov <svu@gnome.org>
+ * Ian Douglas Scott <idscott@system76.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#include <adwaita.h>
+
+#define CC_TYPE_INPUT_LIST_BOX (cc_input_list_box_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputListBox, cc_input_list_box, CC, INPUT_LIST_BOX, AdwBin)
+
+void cc_input_list_box_set_login (CcInputListBox *box,
+ gboolean login);
+void cc_input_list_box_set_login_auto_apply (CcInputListBox *box,
+ gboolean auto_apply);
+void cc_input_list_box_set_localed (CcInputListBox *box,
+ GDBusProxy *localed);
+void cc_input_list_box_set_permission (CcInputListBox *box,
+ GPermission *permission);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-input-list-box.ui b/panels/keyboard/cc-input-list-box.ui
new file mode 100644
index 0000000..036cca4
--- /dev/null
+++ b/panels/keyboard/cc-input-list-box.ui
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcInputListBox" parent="AdwBin">
+
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="selection-mode">none</property>
+ <signal name="row_activated" handler="input_row_activated_cb" object="CcInputListBox" swapped="yes"/>
+ <signal name="keynav_failed" handler="keynav_failed_cb" object="CcInputListBox" swapped="yes"/>
+ <style>
+ <class name="boxed-list"/>
+ </style>
+ <child>
+ <object class="GtkListBoxRow" id="no_inputs_row">
+ <property name="selectable">False</property>
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="margin_bottom">8</property>
+ <property name="margin_top">8</property>
+ <property name="label" translatable="yes">No input source selected</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBoxRow" id="add_input_row">
+ <property name="selectable">False</property>
+ <child>
+ <object class="GtkImage">
+ <property name="hexpand">True</property>
+ <property name="height_request">50</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <property name="icon-name">list-add-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/panels/keyboard/cc-input-row.c b/panels/keyboard/cc-input-row.c
new file mode 100644
index 0000000..9668824
--- /dev/null
+++ b/panels/keyboard/cc-input-row.c
@@ -0,0 +1,306 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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 "cc-input-row.h"
+#include "cc-input-source-ibus.h"
+
+struct _CcInputRow
+{
+ AdwActionRow parent_instance;
+
+ CcInputSource *source;
+
+ GtkListBox *drag_widget;
+
+ GtkDragSource *drag_source;
+ gdouble drag_x;
+ gdouble drag_y;
+};
+
+G_DEFINE_TYPE (CcInputRow, cc_input_row, ADW_TYPE_ACTION_ROW)
+
+enum
+{
+ SIGNAL_SHOW_SETTINGS,
+ SIGNAL_SHOW_LAYOUT,
+ SIGNAL_MOVE_ROW,
+ SIGNAL_REMOVE_ROW,
+ SIGNAL_LAST
+};
+
+static guint signals[SIGNAL_LAST] = { 0, };
+
+static GdkContentProvider *
+drag_prepare_cb (GtkDragSource *source,
+ double x,
+ double y,
+ CcInputRow *self)
+{
+ self->drag_x = x;
+ self->drag_y = y;
+
+ return gdk_content_provider_new_typed (CC_TYPE_INPUT_ROW, self);
+}
+
+static void
+drag_begin_cb (GtkDragSource *source,
+ GdkDrag *drag,
+ CcInputRow *self)
+{
+ GtkAllocation alloc;
+ CcInputRow *drag_row;
+ GtkWidget *drag_icon;
+
+ gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+ self->drag_widget = GTK_LIST_BOX (gtk_list_box_new ());
+ gtk_widget_set_size_request (GTK_WIDGET (self->drag_widget), alloc.width, alloc.height);
+
+ drag_row = cc_input_row_new (self->source);
+ gtk_list_box_append (self->drag_widget, GTK_WIDGET (drag_row));
+ gtk_list_box_drag_highlight_row (self->drag_widget, GTK_LIST_BOX_ROW (drag_row));
+
+ drag_icon = gtk_drag_icon_get_for_drag (drag);
+ gtk_drag_icon_set_child (GTK_DRAG_ICON (drag_icon), GTK_WIDGET (self->drag_widget));
+ gdk_drag_set_hotspot (drag, self->drag_x, self->drag_y);
+}
+
+static gboolean
+drop_cb (GtkDropTarget *drop_target,
+ const GValue *value,
+ gdouble x,
+ gdouble y,
+ CcInputRow *self)
+{
+ CcInputRow *source;
+
+ if (!G_VALUE_HOLDS (value, CC_TYPE_INPUT_ROW))
+ return FALSE;
+
+ source = g_value_get_object (value);
+
+ g_signal_emit (source,
+ signals[SIGNAL_MOVE_ROW],
+ 0,
+ self);
+
+ return TRUE;
+}
+
+static void
+move_up_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ CcInputRow *self = CC_INPUT_ROW (widget);
+ GtkListBox *list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (self)));
+ gint previous_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (self)) - 1;
+ GtkListBoxRow *previous_row = gtk_list_box_get_row_at_index (list_box, previous_idx);
+
+ if (previous_row == NULL)
+ return;
+
+ g_signal_emit (self,
+ signals[SIGNAL_MOVE_ROW],
+ 0,
+ previous_row);
+}
+
+static void
+move_down_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ CcInputRow *self = CC_INPUT_ROW (widget);
+ GtkListBox *list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (self)));
+ gint next_idx = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (self)) + 1;
+ GtkListBoxRow *next_row = gtk_list_box_get_row_at_index (list_box, next_idx);
+
+ if (next_row == NULL)
+ return;
+
+ g_signal_emit (next_row,
+ signals[SIGNAL_MOVE_ROW],
+ 0,
+ self);
+}
+
+static void
+show_settings_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ CcInputRow *self = CC_INPUT_ROW (widget);
+ g_signal_emit (self,
+ signals[SIGNAL_SHOW_SETTINGS],
+ 0);
+}
+
+static void
+show_layout_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ CcInputRow *self = CC_INPUT_ROW (widget);
+ g_signal_emit (self,
+ signals[SIGNAL_SHOW_LAYOUT],
+ 0);
+}
+
+static void
+remove_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ CcInputRow *self = CC_INPUT_ROW (widget);
+ g_signal_emit (self,
+ signals[SIGNAL_REMOVE_ROW],
+ 0);
+}
+
+static void
+cc_input_row_dispose (GObject *object)
+{
+ CcInputRow *self = CC_INPUT_ROW (object);
+
+ g_clear_object (&self->source);
+
+ G_OBJECT_CLASS (cc_input_row_parent_class)->dispose (object);
+}
+
+void
+cc_input_row_class_init (CcInputRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = cc_input_row_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-input-row.ui");
+
+ signals[SIGNAL_SHOW_SETTINGS] =
+ g_signal_new ("show-settings",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_SHOW_LAYOUT] =
+ g_signal_new ("show-layout",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_MOVE_ROW] =
+ g_signal_new ("move-row",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1, CC_TYPE_INPUT_ROW);
+
+ signals[SIGNAL_REMOVE_ROW] =
+ g_signal_new ("remove-row",
+ G_TYPE_FROM_CLASS (object_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_install_action (widget_class, "row.move-up", NULL, move_up_cb);
+ gtk_widget_class_install_action (widget_class, "row.move-down", NULL, move_down_cb);
+ gtk_widget_class_install_action (widget_class, "row.show-layout", NULL, show_layout_cb);
+ gtk_widget_class_install_action (widget_class, "row.show-settings", NULL, show_settings_cb);
+ gtk_widget_class_install_action (widget_class, "row.remove", NULL, remove_cb);
+}
+
+void
+cc_input_row_init (CcInputRow *self)
+{
+ GtkDropTarget *drop_target;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->drag_source = gtk_drag_source_new ();
+ gtk_drag_source_set_actions (self->drag_source, GDK_ACTION_MOVE);
+ g_signal_connect (self->drag_source, "prepare", G_CALLBACK (drag_prepare_cb), self);
+ g_signal_connect (self->drag_source, "drag-begin", G_CALLBACK (drag_begin_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->drag_source));
+
+ drop_target = gtk_drop_target_new (CC_TYPE_INPUT_ROW, GDK_ACTION_MOVE);
+ gtk_drop_target_set_preload (drop_target, TRUE);
+ g_signal_connect (drop_target, "drop", G_CALLBACK (drop_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drop_target));
+}
+
+static void
+label_changed_cb (CcInputRow *self)
+{
+ g_autofree gchar *label = cc_input_source_get_label (self->source);
+ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self), label);
+}
+
+CcInputRow *
+cc_input_row_new (CcInputSource *source)
+{
+ CcInputRow *self;
+
+ self = g_object_new (CC_TYPE_INPUT_ROW, NULL);
+ self->source = g_object_ref (source);
+
+ g_signal_connect_object (source, "label-changed", G_CALLBACK (label_changed_cb), self, G_CONNECT_SWAPPED);
+ label_changed_cb (self);
+
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "row.show-settings", CC_IS_INPUT_SOURCE_IBUS (source));
+
+ return self;
+}
+
+CcInputSource *
+cc_input_row_get_source (CcInputRow *self)
+{
+ g_return_val_if_fail (CC_IS_INPUT_ROW (self), NULL);
+ return self->source;
+}
+
+void
+cc_input_row_set_removable (CcInputRow *self,
+ gboolean removable)
+{
+ g_return_if_fail (CC_IS_INPUT_ROW (self));
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "row.remove", removable);
+}
+
+void
+cc_input_row_set_draggable (CcInputRow *self,
+ gboolean draggable)
+{
+ gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (self->drag_source),
+ draggable ? GTK_PHASE_BUBBLE : GTK_PHASE_NONE);
+}
diff --git a/panels/keyboard/cc-input-row.h b/panels/keyboard/cc-input-row.h
new file mode 100644
index 0000000..666bcb6
--- /dev/null
+++ b/panels/keyboard/cc-input-row.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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 <adwaita.h>
+#include <gtk/gtk.h>
+#include <gio/gdesktopappinfo.h>
+
+#include "cc-input-source.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_ROW (cc_input_row_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputRow, cc_input_row, CC, INPUT_ROW, AdwActionRow)
+
+CcInputRow *cc_input_row_new (CcInputSource *source);
+
+CcInputSource *cc_input_row_get_source (CcInputRow *row);
+
+void cc_input_row_set_removable (CcInputRow *row,
+ gboolean removable);
+
+void cc_input_row_set_draggable (CcInputRow *row,
+ gboolean draggable);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-input-row.ui b/panels/keyboard/cc-input-row.ui
new file mode 100644
index 0000000..5ce33f4
--- /dev/null
+++ b/panels/keyboard/cc-input-row.ui
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="CcInputRow" parent="AdwActionRow">
+ <property name="selectable">False</property>
+ <child type="prefix">
+ <object class="GtkImage">
+ <property name="icon_name">list-drag-handle-symbolic</property>
+ <style>
+ <class name="drag-handle"/>
+ </style>
+ </object>
+ </child>
+ <child type="suffix">
+ <object class="GtkMenuButton">
+ <property name="valign">center</property>
+ <property name="icon_name">view-more-symbolic</property>
+ <property name="menu_model">popover_menu</property>
+ <accessibility>
+ <property name="label" translatable="yes">Options</property>
+ </accessibility>
+ <style>
+ <class name="flat"/>
+ </style>
+ </object>
+ </child>
+ </template>
+ <menu id="popover_menu">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Move Up</attribute>
+ <attribute name="action">row.move-up</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Move Down</attribute>
+ <attribute name="action">row.move-down</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Preferences</attribute>
+ <attribute name="action">row.show-settings</attribute>
+ <attribute name="hidden-when">action-disabled</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">View Keyboard Layout</attribute>
+ <attribute name="action">row.show-layout</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Remove</attribute>
+ <attribute name="action">row.remove</attribute>
+ <attribute name="hidden-when">action-disabled</attribute>
+ </item>
+ </section>
+ </menu>
+</interface>
diff --git a/panels/keyboard/cc-input-source-ibus.c b/panels/keyboard/cc-input-source-ibus.c
new file mode 100644
index 0000000..1aa1ab8
--- /dev/null
+++ b/panels/keyboard/cc-input-source-ibus.c
@@ -0,0 +1,155 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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 "cc-input-source-ibus.h"
+#ifdef HAVE_IBUS
+#include "cc-ibus-utils.h"
+#endif
+
+struct _CcInputSourceIBus
+{
+ CcInputSource parent_instance;
+
+ gchar *engine_name;
+#ifdef HAVE_IBUS
+ IBusEngineDesc *engine_desc;
+#endif
+};
+
+G_DEFINE_TYPE (CcInputSourceIBus, cc_input_source_ibus, CC_TYPE_INPUT_SOURCE)
+
+static gchar *
+cc_input_source_ibus_get_label (CcInputSource *source)
+{
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (source);
+#ifdef HAVE_IBUS
+ if (self->engine_desc)
+ return g_strdup (engine_get_display_name (self->engine_desc));
+ else
+#endif
+ return g_strdup (self->engine_name);
+}
+
+static gboolean
+cc_input_source_ibus_matches (CcInputSource *source,
+ CcInputSource *source2)
+{
+ if (!CC_IS_INPUT_SOURCE_IBUS (source2))
+ return FALSE;
+
+ return g_strcmp0 (CC_INPUT_SOURCE_IBUS (source)->engine_name, CC_INPUT_SOURCE_IBUS (source2)->engine_name) == 0;
+}
+
+static const gchar *
+cc_input_source_ibus_get_layout (CcInputSource *source)
+{
+#ifdef HAVE_IBUS
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (source);
+ if (self->engine_desc != NULL)
+ return ibus_engine_desc_get_layout (self->engine_desc);
+ else
+#endif
+ return NULL;
+}
+
+static const gchar *
+cc_input_source_ibus_get_layout_variant (CcInputSource *source)
+{
+#ifdef HAVE_IBUS
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (source);
+ if (self->engine_desc != NULL)
+ return ibus_engine_desc_get_layout_variant (self->engine_desc);
+ else
+#endif
+ return NULL;
+}
+
+static void
+cc_input_source_ibus_dispose (GObject *object)
+{
+ CcInputSourceIBus *self = CC_INPUT_SOURCE_IBUS (object);
+
+ g_clear_pointer (&self->engine_name, g_free);
+#ifdef HAVE_IBUS
+ g_clear_object (&self->engine_desc);
+#endif
+
+ G_OBJECT_CLASS (cc_input_source_ibus_parent_class)->dispose (object);
+}
+
+void
+cc_input_source_ibus_class_init (CcInputSourceIBusClass *klass)
+{
+ CcInputSourceClass *input_source_class = CC_INPUT_SOURCE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ input_source_class->get_label = cc_input_source_ibus_get_label;
+ input_source_class->matches = cc_input_source_ibus_matches;
+ input_source_class->get_layout = cc_input_source_ibus_get_layout;
+ input_source_class->get_layout_variant = cc_input_source_ibus_get_layout_variant;
+ object_class->dispose = cc_input_source_ibus_dispose;
+}
+
+void
+cc_input_source_ibus_init (CcInputSourceIBus *source)
+{
+}
+
+CcInputSourceIBus *
+cc_input_source_ibus_new (const gchar *engine_name)
+{
+ CcInputSourceIBus *source;
+
+ source = g_object_new (CC_TYPE_INPUT_SOURCE_IBUS, NULL);
+ source->engine_name = g_strdup (engine_name);
+
+ return source;
+}
+
+#ifdef HAVE_IBUS
+void
+cc_input_source_ibus_set_engine_desc (CcInputSourceIBus *source,
+ IBusEngineDesc *engine_desc)
+{
+ g_return_if_fail (CC_IS_INPUT_SOURCE_IBUS (source));
+
+ g_clear_object (&source->engine_desc);
+ source->engine_desc = g_object_ref (engine_desc);
+ cc_input_source_emit_label_changed (CC_INPUT_SOURCE (source));
+}
+#endif
+
+const gchar *
+cc_input_source_ibus_get_engine_name (CcInputSourceIBus *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE_IBUS (source), NULL);
+ return source->engine_name;
+}
+
+GDesktopAppInfo *
+cc_input_source_ibus_get_app_info (CcInputSourceIBus *source)
+{
+ g_auto(GStrv) tokens = NULL;
+ g_autofree gchar *desktop_file_name = NULL;
+
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE_IBUS (source), NULL);
+
+ tokens = g_strsplit (source->engine_name, ":", 2);
+ desktop_file_name = g_strdup_printf ("ibus-setup-%s.desktop", tokens[0]);
+
+ return g_desktop_app_info_new (desktop_file_name);
+}
diff --git a/panels/keyboard/cc-input-source-ibus.h b/panels/keyboard/cc-input-source-ibus.h
new file mode 100644
index 0000000..2c09d01
--- /dev/null
+++ b/panels/keyboard/cc-input-source-ibus.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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 <config.h>
+
+#ifdef HAVE_IBUS
+#include <ibus.h>
+#endif
+
+#include <gio/gdesktopappinfo.h>
+
+#include "cc-input-source.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_SOURCE_IBUS (cc_input_source_ibus_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputSourceIBus, cc_input_source_ibus, CC, INPUT_SOURCE_IBUS, CcInputSource)
+
+CcInputSourceIBus *cc_input_source_ibus_new (const gchar *engine_name);
+
+#ifdef HAVE_IBUS
+void cc_input_source_ibus_set_engine_desc (CcInputSourceIBus *source,
+ IBusEngineDesc *engine_desc);
+#endif
+
+const gchar *cc_input_source_ibus_get_engine_name (CcInputSourceIBus *source);
+
+GDesktopAppInfo *cc_input_source_ibus_get_app_info (CcInputSourceIBus *source);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-input-source-xkb.c b/panels/keyboard/cc-input-source-xkb.c
new file mode 100644
index 0000000..2ea30be
--- /dev/null
+++ b/panels/keyboard/cc-input-source-xkb.c
@@ -0,0 +1,134 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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 "cc-input-source-xkb.h"
+
+struct _CcInputSourceXkb
+{
+ CcInputSource parent_instance;
+
+ GnomeXkbInfo *xkb_info;
+ gchar *layout;
+ gchar *variant;
+};
+
+G_DEFINE_TYPE (CcInputSourceXkb, cc_input_source_xkb, CC_TYPE_INPUT_SOURCE)
+
+static gchar *
+cc_input_source_xkb_get_label (CcInputSource *source)
+{
+ CcInputSourceXkb *self = CC_INPUT_SOURCE_XKB (source);
+ g_autofree gchar *id = NULL;
+ const gchar *name;
+
+ id = cc_input_source_xkb_get_id (self);
+ gnome_xkb_info_get_layout_info (self->xkb_info, id, &name, NULL, NULL, NULL);
+ if (name)
+ return g_strdup (name);
+ else
+ return g_strdup (id);
+}
+
+static gboolean
+cc_input_source_xkb_matches (CcInputSource *source,
+ CcInputSource *source2)
+{
+ if (!CC_IS_INPUT_SOURCE_XKB (source2))
+ return FALSE;
+
+ return g_strcmp0 (CC_INPUT_SOURCE_XKB (source)->layout, CC_INPUT_SOURCE_XKB (source2)->layout) == 0 &&
+ g_strcmp0 (CC_INPUT_SOURCE_XKB (source)->variant, CC_INPUT_SOURCE_XKB (source2)->variant) == 0;
+}
+
+static void
+cc_input_source_xkb_dispose (GObject *object)
+{
+ CcInputSourceXkb *self = CC_INPUT_SOURCE_XKB (object);
+
+ g_clear_object (&self->xkb_info);
+ g_clear_pointer (&self->layout, g_free);
+ g_clear_pointer (&self->variant, g_free);
+
+ G_OBJECT_CLASS (cc_input_source_xkb_parent_class)->dispose (object);
+}
+
+static const gchar *
+cc_input_source_xkb_get_layout (CcInputSource *source)
+{
+ return CC_INPUT_SOURCE_XKB (source)->layout;
+}
+
+static const gchar *
+cc_input_source_xkb_get_layout_variant (CcInputSource *source)
+{
+ return CC_INPUT_SOURCE_XKB (source)->variant;
+}
+
+void
+cc_input_source_xkb_class_init (CcInputSourceXkbClass *klass)
+{
+ CcInputSourceClass *input_source_class = CC_INPUT_SOURCE_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ input_source_class->get_label = cc_input_source_xkb_get_label;
+ input_source_class->matches = cc_input_source_xkb_matches;
+ input_source_class->get_layout = cc_input_source_xkb_get_layout;
+ input_source_class->get_layout_variant = cc_input_source_xkb_get_layout_variant;
+ object_class->dispose = cc_input_source_xkb_dispose;
+}
+
+void
+cc_input_source_xkb_init (CcInputSourceXkb *source)
+{
+}
+
+CcInputSourceXkb *
+cc_input_source_xkb_new (GnomeXkbInfo *xkb_info,
+ const gchar *layout,
+ const gchar *variant)
+{
+ CcInputSourceXkb *source;
+
+ source = g_object_new (CC_TYPE_INPUT_SOURCE_XKB, NULL);
+ source->xkb_info = g_object_ref (xkb_info);
+ source->layout = g_strdup (layout);
+ source->variant = g_strdup (variant);
+
+ return source;
+}
+
+CcInputSourceXkb *
+cc_input_source_xkb_new_from_id (GnomeXkbInfo *xkb_info,
+ const gchar *id)
+{
+ g_auto(GStrv) tokens = NULL;
+
+ tokens = g_strsplit (id, "+", 2);
+
+ return cc_input_source_xkb_new (xkb_info, tokens[0], tokens[1]);
+}
+
+gchar *
+cc_input_source_xkb_get_id (CcInputSourceXkb *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE_XKB (source), NULL);
+ if (source->variant != NULL)
+ return g_strdup_printf ("%s+%s", source->layout, source->variant);
+ else
+ return g_strdup (source->layout);
+}
diff --git a/panels/keyboard/cc-input-source-xkb.h b/panels/keyboard/cc-input-source-xkb.h
new file mode 100644
index 0000000..e8886d0
--- /dev/null
+++ b/panels/keyboard/cc-input-source-xkb.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-xkb-info.h>
+
+#include "cc-input-source.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_SOURCE_XKB (cc_input_source_xkb_get_type ())
+G_DECLARE_FINAL_TYPE (CcInputSourceXkb, cc_input_source_xkb, CC, INPUT_SOURCE_XKB, CcInputSource)
+
+CcInputSourceXkb *cc_input_source_xkb_new (GnomeXkbInfo *xkb_info,
+ const gchar *layout,
+ const gchar *variant);
+
+CcInputSourceXkb *cc_input_source_xkb_new_from_id (GnomeXkbInfo *xkb_info,
+ const gchar *id);
+
+gchar *cc_input_source_xkb_get_id (CcInputSourceXkb *source);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-input-source.c b/panels/keyboard/cc-input-source.c
new file mode 100644
index 0000000..df8db8b
--- /dev/null
+++ b/panels/keyboard/cc-input-source.c
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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 "cc-input-source.h"
+
+enum
+{
+ SIGNAL_LABEL_CHANGED,
+ SIGNAL_LAST
+};
+
+static guint signals[SIGNAL_LAST] = {0};
+
+G_DEFINE_TYPE (CcInputSource, cc_input_source, G_TYPE_OBJECT)
+
+void
+cc_input_source_class_init (CcInputSourceClass *klass)
+{
+ signals[SIGNAL_LABEL_CHANGED] =
+ g_signal_new ("label-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+void
+cc_input_source_init (CcInputSource *source)
+{
+}
+
+void
+cc_input_source_emit_label_changed (CcInputSource *source)
+{
+ g_return_if_fail (CC_IS_INPUT_SOURCE (source));
+ g_signal_emit (source, signals[SIGNAL_LABEL_CHANGED], 0);
+}
+
+gchar *
+cc_input_source_get_label (CcInputSource *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), NULL);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->get_label (source);
+}
+
+gboolean
+cc_input_source_matches (CcInputSource *source,
+ CcInputSource *source2)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), FALSE);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->matches (source, source2);
+}
+
+const gchar *
+cc_input_source_get_layout (CcInputSource *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), NULL);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->get_layout (source);
+}
+
+const gchar *
+cc_input_source_get_layout_variant (CcInputSource *source)
+{
+ g_return_val_if_fail (CC_IS_INPUT_SOURCE (source), NULL);
+ return CC_INPUT_SOURCE_GET_CLASS (source)->get_layout_variant (source);
+}
diff --git a/panels/keyboard/cc-input-source.h b/panels/keyboard/cc-input-source.h
new file mode 100644
index 0000000..5b7865d
--- /dev/null
+++ b/panels/keyboard/cc-input-source.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2018 Canonical Ltd.
+ *
+ * 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>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_INPUT_SOURCE (cc_input_source_get_type ())
+G_DECLARE_DERIVABLE_TYPE (CcInputSource, cc_input_source, CC, INPUT_SOURCE, GObject)
+
+struct _CcInputSourceClass
+{
+ GObjectClass parent_class;
+
+ gchar* (*get_label) (CcInputSource *source);
+ gboolean (*matches) (CcInputSource *source,
+ CcInputSource *source2);
+ const gchar* (*get_layout) (CcInputSource *source);
+ const gchar* (*get_layout_variant) (CcInputSource *source);
+};
+
+void cc_input_source_emit_label_changed (CcInputSource *source);
+
+gchar *cc_input_source_get_label (CcInputSource *source);
+
+gboolean cc_input_source_matches (CcInputSource *source,
+ CcInputSource *source2);
+
+const gchar *cc_input_source_get_layout (CcInputSource *source);
+
+const gchar *cc_input_source_get_layout_variant (CcInputSource *source);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-keyboard-item.c b/panels/keyboard/cc-keyboard-item.c
new file mode 100644
index 0000000..31b134a
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-item.c
@@ -0,0 +1,881 @@
+/* -*- 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;
+
+ CcKeyboardItem *reverse_item;
+ gboolean is_reversed;
+ gboolean hidden;
+
+ CcKeyboardItemType type;
+
+ BindingGroupType group;
+ char *description;
+ gboolean editable;
+ GList *key_combos;
+ GList *default_combos;
+ gboolean can_set_multiple;
+
+ /* GSettings path */
+ char *gsettings_path;
+ gboolean desc_editable;
+ char *command;
+ gboolean cmd_editable;
+
+ /* GSettings */
+ char *schema;
+ char *key;
+ GSettings *settings;
+};
+
+enum
+{
+ PROP_0,
+ PROP_DESCRIPTION,
+ PROP_EDITABLE,
+ PROP_TYPE,
+ PROP_IS_VALUE_DEFAULT,
+ PROP_COMMAND,
+ PROP_KEY_COMBOS
+};
+
+static void cc_keyboard_item_class_init (CcKeyboardItemClass *klass);
+static void cc_keyboard_item_init (CcKeyboardItem *keyboard_item);
+static void cc_keyboard_item_finalize (GObject *object);
+
+G_DEFINE_TYPE (CcKeyboardItem, cc_keyboard_item, G_TYPE_OBJECT)
+
+static const CcKeyCombo EMPTY_COMBO = { 0, 0, 0 };
+
+static gboolean
+combo_equal (CcKeyCombo *a, CcKeyCombo *b)
+{
+ return (a->keyval == b->keyval
+ && a->keycode == b->keycode
+ && a->mask == b->mask);
+}
+
+static gboolean
+combos_contains (GList *combos, CcKeyCombo *needle)
+{
+ for (GList *l = combos; l != NULL; l = l->next)
+ {
+ if (combo_equal (l->data, needle))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+combos_equal (GList *a, GList *b)
+{
+ // Should be efficient enough for any sane number of bindings
+
+ for (GList *l = a; l != NULL; l = l->next)
+ {
+ if (!combos_contains (b, l->data))
+ return FALSE;
+ }
+
+ for (GList *l = b; l != NULL; l = l->next)
+ {
+ if (!combos_contains (a, l->data))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+binding_from_string (const char *str,
+ CcKeyCombo *combo)
+{
+ g_return_val_if_fail (combo != NULL, FALSE);
+ g_autofree guint *keycodes = NULL;
+
+ if (str == NULL || strcmp (str, "disabled") == 0)
+ {
+ memset (combo, 0, sizeof(CcKeyCombo));
+ return TRUE;
+ }
+
+ gtk_accelerator_parse_with_keycode (str,
+ gdk_display_get_default (),
+ &combo->keyval,
+ &keycodes,
+ &combo->mask);
+
+ combo->keycode = (keycodes ? keycodes[0] : 0);
+
+ if (combo->keyval == 0)
+ return FALSE;
+ else
+ return TRUE;
+}
+
+static void
+_set_description (CcKeyboardItem *item,
+ const char *value)
+{
+ g_free (item->description);
+ item->description = g_strdup (value);
+}
+
+const char *
+cc_keyboard_item_get_description (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+
+ return item->description;
+}
+
+gboolean
+cc_keyboard_item_get_desc_editable (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE);
+
+ return item->desc_editable;
+}
+
+static void
+_set_type (CcKeyboardItem *item,
+ gint value)
+{
+ item->type = value;
+}
+
+static void
+_set_command (CcKeyboardItem *item,
+ const char *value)
+{
+ g_free (item->command);
+ item->command = g_strdup (value);
+}
+
+const char *
+cc_keyboard_item_get_command (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+
+ return item->command;
+}
+
+gboolean
+cc_keyboard_item_get_cmd_editable (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE);
+
+ return item->cmd_editable;
+}
+
+static void
+cc_keyboard_item_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ CcKeyboardItem *self;
+
+ self = CC_KEYBOARD_ITEM (object);
+
+ switch (prop_id) {
+ case PROP_DESCRIPTION:
+ _set_description (self, g_value_get_string (value));
+ break;
+ case PROP_COMMAND:
+ _set_command (self, g_value_get_string (value));
+ break;
+ case PROP_TYPE:
+ _set_type (self, g_value_get_int (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_keyboard_item_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ CcKeyboardItem *self;
+
+ self = CC_KEYBOARD_ITEM (object);
+
+ switch (prop_id) {
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, self->description);
+ break;
+ case PROP_EDITABLE:
+ g_value_set_boolean (value, self->editable);
+ break;
+ case PROP_COMMAND:
+ g_value_set_string (value, self->command);
+ break;
+ case PROP_IS_VALUE_DEFAULT:
+ g_value_set_boolean (value, cc_keyboard_item_is_value_default (self));
+ break;
+ case PROP_KEY_COMBOS:
+ g_value_set_pointer (value, self->key_combos);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+cc_keyboard_item_class_init (CcKeyboardItemClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = cc_keyboard_item_get_property;
+ object_class->set_property = cc_keyboard_item_set_property;
+ object_class->finalize = cc_keyboard_item_finalize;
+
+ g_object_class_install_property (object_class,
+ PROP_DESCRIPTION,
+ g_param_spec_string ("description",
+ "description",
+ "description",
+ NULL,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_EDITABLE,
+ g_param_spec_boolean ("editable",
+ NULL,
+ NULL,
+ FALSE,
+ G_PARAM_READABLE));
+
+ g_object_class_install_property (object_class,
+ PROP_TYPE,
+ g_param_spec_int ("type",
+ NULL,
+ NULL,
+ CC_KEYBOARD_ITEM_TYPE_NONE,
+ CC_KEYBOARD_ITEM_TYPE_GSETTINGS,
+ CC_KEYBOARD_ITEM_TYPE_NONE,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE));
+
+ g_object_class_install_property (object_class,
+ PROP_COMMAND,
+ g_param_spec_string ("command",
+ "command",
+ "command",
+ NULL,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_IS_VALUE_DEFAULT,
+ g_param_spec_boolean ("is-value-default",
+ "is value default",
+ "is value default",
+ TRUE,
+ G_PARAM_READABLE));
+
+ g_object_class_install_property (object_class,
+ PROP_KEY_COMBOS,
+ g_param_spec_pointer ("key-combos",
+ "key combos",
+ "key combos",
+ G_PARAM_READABLE));
+}
+
+static void
+cc_keyboard_item_init (CcKeyboardItem *item)
+{
+}
+
+static void
+cc_keyboard_item_finalize (GObject *object)
+{
+ CcKeyboardItem *item;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (CC_IS_KEYBOARD_ITEM (object));
+
+ item = CC_KEYBOARD_ITEM (object);
+
+ if (item->settings != NULL)
+ g_object_unref (item->settings);
+
+ /* Free memory */
+ g_free (item->gsettings_path);
+ g_free (item->description);
+ g_free (item->command);
+ g_free (item->schema);
+ g_free (item->key);
+ g_list_free_full (item->key_combos, g_free);
+ g_list_free_full (item->default_combos, g_free);
+
+ G_OBJECT_CLASS (cc_keyboard_item_parent_class)->finalize (object);
+}
+
+CcKeyboardItem *
+cc_keyboard_item_new (CcKeyboardItemType type)
+{
+ GObject *object;
+
+ object = g_object_new (CC_TYPE_KEYBOARD_ITEM,
+ "type", type,
+ NULL);
+
+ return CC_KEYBOARD_ITEM (object);
+}
+
+static guint *
+get_above_tab_keysyms (void)
+{
+ guint keycode = 0x29 /* KEY_GRAVE */ + 8;
+ g_autofree guint *keyvals = NULL;
+ GArray *keysyms;
+ int n_entries, i, j;
+
+ keysyms = g_array_new (TRUE, FALSE, sizeof (guint));
+
+ if (!gdk_display_map_keycode (gdk_display_get_default (),
+ keycode,
+ NULL,
+ &keyvals,
+ &n_entries))
+ goto out;
+
+ for (i = 0; i < n_entries; i++)
+ {
+ gboolean found = FALSE;
+
+ for (j = 0; j < keysyms->len; j++)
+ if (g_array_index (keysyms, guint, j) == keyvals[i])
+ {
+ found = TRUE;
+ break;
+ }
+
+ if (!found)
+ g_array_append_val (keysyms, keyvals[i]);
+ }
+
+out:
+ return (guint *)g_array_free (keysyms, FALSE);
+}
+
+/*
+ * translate_above_tab:
+ *
+ * @original_bindings: A list of accelerator strings
+ * @new_bindings: (out): Translated bindings if translation is needed
+ *
+ * Translate accelerator strings that contain the Above_Tab fake keysym
+ * used by mutter to strings that use the real keysyms that correspond
+ * to the key that is located physically above the tab key.
+ *
+ * Returns: %TRUE if strings were translated, %FALSE if @original_bindings
+ * can be used unmodified
+ */
+static gboolean
+translate_above_tab (char **original_bindings,
+ char ***new_bindings)
+{
+ GPtrArray *replaced_bindings;
+ g_autofree guint *above_tab_keysyms = NULL;
+ gboolean needs_translation = FALSE;
+ char **str;
+
+ for (str = original_bindings; *str && !needs_translation; str++)
+ needs_translation = strstr (*str, "Above_Tab") != NULL;
+
+ if (!needs_translation)
+ return FALSE;
+
+ above_tab_keysyms = get_above_tab_keysyms ();
+
+ replaced_bindings = g_ptr_array_new ();
+
+ for (str = original_bindings; *str; str++)
+ {
+ if (strstr (*str, "Above_Tab") == NULL)
+ {
+ g_ptr_array_add (replaced_bindings, g_strdup (*str));
+ }
+ else
+ {
+ g_auto (GStrv) split_str = g_strsplit (*str, "Above_Tab", -1);
+ int i;
+
+ for (i = 0; above_tab_keysyms[i]; i++)
+ {
+ g_autofree char *sym = NULL;
+
+ sym = gtk_accelerator_name (above_tab_keysyms[i], 0);
+ g_ptr_array_add (replaced_bindings, g_strjoinv (sym, split_str));
+ }
+ }
+ g_ptr_array_add (replaced_bindings, NULL);
+ }
+
+ *new_bindings = (char **)g_ptr_array_free (replaced_bindings, FALSE);
+ return TRUE;
+}
+
+static GList *
+variant_get_key_combos (GVariant *variant)
+{
+ GList *combos = NULL;
+ char **translated_bindings, **str;
+ g_auto(GStrv) bindings = NULL;
+
+ if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING))
+ {
+ bindings = g_malloc0_n (2, sizeof(char *));
+ if (g_strcmp0 (g_variant_get_string (variant, NULL), "") != 0)
+ bindings[0] = g_variant_dup_string (variant, NULL);
+ }
+ else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY))
+ {
+ bindings = g_variant_dup_strv (variant, NULL);
+ }
+
+ if (translate_above_tab (bindings, &translated_bindings))
+ {
+ g_strfreev (bindings);
+ bindings = translated_bindings;
+ }
+
+ for (str = bindings; *str; str++)
+ {
+ g_autofree CcKeyCombo *combo = g_new (CcKeyCombo, 1);
+
+ binding_from_string (*str, combo);
+
+ if (combo->keyval != 0 || combo->keycode != 0 || combo->mask != 0)
+ combos = g_list_prepend (combos, g_steal_pointer (&combo));
+ }
+
+ return g_list_reverse (combos);
+}
+
+static GList *
+settings_get_key_combos (GSettings *settings,
+ const char *key,
+ gboolean use_default)
+{
+ GList *key_combos;
+ g_autoptr(GVariant) variant = NULL;
+
+ if (use_default)
+ variant = g_settings_get_default_value (settings, key);
+ else
+ variant = g_settings_get_value (settings, key);
+ key_combos = variant_get_key_combos (variant);
+
+ return key_combos;
+}
+
+static void
+binding_changed (CcKeyboardItem *item,
+ const char *key)
+{
+ g_list_free_full (item->key_combos, g_free);
+ item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);
+
+ item->editable = g_settings_is_writable (item->settings, item->key);
+
+ g_object_notify (G_OBJECT (item), "key-combos");
+}
+
+gboolean
+cc_keyboard_item_load_from_gsettings_path (CcKeyboardItem *item,
+ const char *path,
+ gboolean reset)
+{
+ g_autoptr(GVariant) variant = NULL;
+
+ item->schema = g_strdup (CUSTOM_KEYS_SCHEMA);
+ item->gsettings_path = g_strdup (path);
+ item->key = g_strdup ("binding");
+ item->settings = g_settings_new_with_path (item->schema, path);
+ item->editable = g_settings_is_writable (item->settings, item->key);
+ item->desc_editable = g_settings_is_writable (item->settings, "name");
+ item->cmd_editable = g_settings_is_writable (item->settings, "command");
+
+ variant = g_settings_get_value (item->settings, item->key);
+ item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY);
+
+ if (reset)
+ {
+ g_settings_reset (item->settings, "name");
+ g_settings_reset (item->settings, "command");
+ g_settings_reset (item->settings, "binding");
+ }
+
+ g_settings_bind (item->settings, "name",
+ G_OBJECT (item), "description", G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (item->settings, "command",
+ G_OBJECT (item), "command", G_SETTINGS_BIND_DEFAULT);
+
+ g_list_free_full (item->key_combos, g_free);
+ item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);
+
+ g_signal_connect_object (G_OBJECT (item->settings), "changed::binding",
+ G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED);
+
+ return TRUE;
+}
+
+gboolean
+cc_keyboard_item_load_from_gsettings (CcKeyboardItem *item,
+ const char *description,
+ const char *schema,
+ const char *key)
+{
+ g_autofree char *signal_name = NULL;
+ g_autoptr(GVariant) variant = NULL;
+
+ item->schema = g_strdup (schema);
+ item->key = g_strdup (key);
+ item->description = g_strdup (description);
+
+ item->settings = g_settings_new (item->schema);
+ item->editable = g_settings_is_writable (item->settings, item->key);
+
+ g_list_free_full (item->key_combos, g_free);
+ item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE);
+
+ g_list_free_full (item->default_combos, g_free);
+ item->default_combos = settings_get_key_combos (item->settings, item->key, TRUE);
+
+ variant = g_settings_get_value (item->settings, item->key);
+ item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY);
+
+ signal_name = g_strdup_printf ("changed::%s", item->key);
+ g_signal_connect_object (G_OBJECT (item->settings), signal_name,
+ G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED);
+
+ return TRUE;
+}
+
+gboolean
+cc_keyboard_item_equal (CcKeyboardItem *a,
+ CcKeyboardItem *b)
+{
+ if (a->type != b->type)
+ return FALSE;
+ switch (a->type)
+ {
+ case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH:
+ return g_str_equal (a->gsettings_path, b->gsettings_path);
+ case CC_KEYBOARD_ITEM_TYPE_GSETTINGS:
+ return (g_str_equal (a->schema, b->schema) &&
+ g_str_equal (a->key, b->key));
+ default:
+ g_assert_not_reached ();
+ }
+
+}
+
+void
+cc_keyboard_item_add_reverse_item (CcKeyboardItem *item,
+ CcKeyboardItem *reverse_item,
+ gboolean is_reversed)
+{
+ g_return_if_fail (item->key != NULL);
+
+ item->reverse_item = reverse_item;
+ if (reverse_item->reverse_item == NULL)
+ {
+ reverse_item->reverse_item = item;
+ reverse_item->is_reversed = !is_reversed;
+ }
+ else
+ g_warn_if_fail (reverse_item->is_reversed == !!is_reversed);
+
+ item->is_reversed = !!is_reversed;
+}
+
+CcKeyboardItem *
+cc_keyboard_item_get_reverse_item (CcKeyboardItem *item)
+{
+ return item->reverse_item;
+}
+
+
+void
+cc_keyboard_item_set_hidden (CcKeyboardItem *item, gboolean hidden)
+{
+ item->hidden = !!hidden;
+}
+
+
+gboolean
+cc_keyboard_item_is_hidden (CcKeyboardItem *item)
+{
+ return item->hidden;
+}
+
+/**
+ * cc_keyboard_item_is_value_default:
+ * @self: a #CcKeyboardItem
+ *
+ * Retrieves whether the shortcut is the default value or not.
+ *
+ * Returns: %TRUE if the shortcut is the default value, %FALSE otherwise.
+ */
+gboolean
+cc_keyboard_item_is_value_default (CcKeyboardItem *self)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (self), FALSE);
+
+ /*
+ * When the shortcut is custom, we don't treat it as modified
+ * since we don't know what would be its default value.
+ */
+ if (self->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
+ return TRUE;
+
+ return combos_equal (self->default_combos, self->key_combos);
+}
+
+/**
+ * cc_keyboard_item_reset:
+ * @self: a #CcKeyboardItem
+ *
+ * Reset the keyboard binding to the default value.
+ */
+void
+cc_keyboard_item_reset (CcKeyboardItem *self)
+{
+ CcKeyboardItem *reverse;
+
+ g_return_if_fail (CC_IS_KEYBOARD_ITEM (self));
+
+ reverse = self->reverse_item;
+
+ g_settings_reset (self->settings, self->key);
+ g_object_notify (G_OBJECT (self), "is-value-default");
+
+ /* Also reset the reverse item */
+ if (reverse)
+ {
+ g_settings_reset (reverse->settings, reverse->key);
+ g_object_notify (G_OBJECT (reverse), "is-value-default");
+ }
+}
+
+GList *
+cc_keyboard_item_get_key_combos (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+ return item->key_combos;
+}
+
+GList *
+cc_keyboard_item_get_default_combos (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+ return item->default_combos;
+}
+
+CcKeyCombo
+cc_keyboard_item_get_primary_combo (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), EMPTY_COMBO);
+ return (item->key_combos) ? *((CcKeyCombo*)item->key_combos->data) : EMPTY_COMBO;
+}
+
+const gchar *
+cc_keyboard_item_get_key (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+ return item->key;
+}
+
+CcKeyboardItemType
+cc_keyboard_item_get_item_type (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), CC_KEYBOARD_ITEM_TYPE_NONE);
+ return item->type;
+}
+
+const gchar *
+cc_keyboard_item_get_gsettings_path (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+ return item->gsettings_path;
+}
+
+GSettings *
+cc_keyboard_item_get_settings (CcKeyboardItem *item)
+{
+ g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL);
+ return item->settings;
+}
+
+gboolean
+cc_keyboard_item_can_set_multiple (CcKeyboardItem *item)
+{
+ return item->can_set_multiple;
+}
+
+static gchar*
+combo_get_accelerator (CcKeyCombo *combo)
+{
+ return gtk_accelerator_name_with_keycode (NULL,
+ combo->keyval,
+ combo->keycode,
+ combo->mask);
+}
+
+static void
+cc_keyboard_item_add_key_combo_inner (CcKeyboardItem *self,
+ CcKeyCombo *combo)
+{
+ g_auto(GStrv) strv = NULL;
+ int i;
+
+ if (!self->can_set_multiple)
+ {
+ g_settings_set_string (self->settings, self->key, combo_get_accelerator (combo));
+ }
+ else
+ {
+ strv = g_new0 (gchar*, g_list_length (self->key_combos) + 2);
+
+ i = 0;
+ for (GList *l = self->key_combos; l != NULL; l = l->next, i++)
+ {
+ if (combo_equal (l->data, combo))
+ // This combo is already in the list
+ return;
+ strv[i] = combo_get_accelerator (l->data);
+ }
+ strv[i] = combo_get_accelerator (combo);
+
+ g_settings_set_strv (self->settings, self->key, (const gchar **)strv);
+ }
+
+ binding_changed (self, self->key);
+}
+
+void
+cc_keyboard_item_add_key_combo (CcKeyboardItem *self,
+ CcKeyCombo *combo)
+{
+ CcKeyCombo reverse_combo;
+
+ if (self->reverse_item)
+ {
+ reverse_combo.keyval = combo->keyval;
+ reverse_combo.keycode = combo->keycode;
+ reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK;
+ cc_keyboard_item_add_key_combo_inner (self->reverse_item, &reverse_combo);
+ }
+
+ cc_keyboard_item_add_key_combo_inner (self, combo);
+}
+
+static void
+cc_keyboard_item_remove_key_combo_inner (CcKeyboardItem *self,
+ CcKeyCombo *combo)
+{
+ g_auto(GStrv) strv = NULL;
+ gboolean found;
+ int i;
+
+ strv = g_new0 (gchar*, g_list_length (self->key_combos) + 1);
+
+ found = FALSE;
+ i = 0;
+ for (GList *l = self->key_combos; l != NULL; l = l->next, i++)
+ {
+ if (combo_equal (l->data, combo))
+ {
+ i--;
+ found = TRUE;
+ }
+ else
+ {
+ strv[i] = combo_get_accelerator (l->data);
+ }
+ }
+
+ if (found)
+ {
+ if (self->can_set_multiple)
+ g_settings_set_strv (self->settings, self->key, (const gchar **)strv);
+ else
+ g_settings_set_string (self->settings, self->key, "");
+ }
+
+ binding_changed (self, self->key);
+}
+
+void
+cc_keyboard_item_remove_key_combo (CcKeyboardItem *self,
+ CcKeyCombo *combo)
+{
+ CcKeyCombo reverse_combo;
+
+ if (self->reverse_item)
+ {
+ reverse_combo.keyval = combo->keyval;
+ reverse_combo.keycode = combo->keycode;
+ reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK;
+ cc_keyboard_item_remove_key_combo_inner (self->reverse_item, &reverse_combo);
+ }
+
+ cc_keyboard_item_remove_key_combo_inner (self, combo);
+}
+
+void cc_keyboard_item_disable (CcKeyboardItem *self)
+{
+ if (!self->can_set_multiple)
+ {
+ g_settings_set_string (self->settings, self->key, "");
+ if (self->reverse_item)
+ g_settings_set_string (self->reverse_item->settings, self->reverse_item->key, "");
+ }
+ else
+ {
+ g_settings_set_strv (self->settings, self->key, NULL);
+ if (self->reverse_item)
+ g_settings_set_strv (self->reverse_item->settings, self->reverse_item->key, NULL);
+ }
+
+ binding_changed (self, self->key);
+}
diff --git a/panels/keyboard/cc-keyboard-item.h b/panels/keyboard/cc-keyboard-item.h
new file mode 100644
index 0000000..2f4194d
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-item.h
@@ -0,0 +1,112 @@
+/* -*- 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);
+
+gboolean cc_keyboard_item_can_set_multiple (CcKeyboardItem *self);
+
+void cc_keyboard_item_add_key_combo (CcKeyboardItem *self,
+ CcKeyCombo *combo);
+
+void cc_keyboard_item_remove_key_combo (CcKeyboardItem *self,
+ CcKeyCombo *combo);
+void cc_keyboard_item_disable (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..04f5653
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-manager.c
@@ -0,0 +1,1050 @@
+/*
+ * 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 <gdk/gdk.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/x11/gdkx.h>
+#include <X11/Xatom.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;
+};
+
+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 methods
+ */
+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, &sections_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,
+ &sections_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, &sections_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, &sections_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);
+}
+
+#ifdef GDK_WINDOWING_X11
+static char *
+get_window_manager_property (GdkDisplay *display,
+ Atom atom,
+ Window window)
+{
+ Display *xdisplay;
+ Atom utf8_string;
+ int result;
+ Atom actual_type;
+ int actual_format;
+ unsigned long n_items;
+ unsigned long bytes_after;
+ unsigned char *prop;
+ char *value;
+
+ if (window == None)
+ return NULL;
+
+ xdisplay = gdk_x11_display_get_xdisplay (display);
+ utf8_string = XInternAtom (xdisplay, "UTF8_STRING", False);
+
+ gdk_x11_display_error_trap_push (display);
+
+ result = XGetWindowProperty (xdisplay,
+ window,
+ atom,
+ 0,
+ G_MAXLONG,
+ False,
+ utf8_string,
+ &actual_type,
+ &actual_format,
+ &n_items,
+ &bytes_after,
+ &prop);
+
+ gdk_x11_display_error_trap_pop_ignored (display);
+
+ if (result != Success ||
+ actual_type != utf8_string ||
+ actual_format != 8 ||
+ n_items == 0)
+ {
+ XFree (prop);
+ return NULL;
+ }
+
+ value = g_strndup ((const char *) prop, n_items);
+ XFree (prop);
+
+ if (!g_utf8_validate (value, -1, NULL))
+ {
+ g_free (value);
+ return NULL;
+ }
+
+ return value;
+}
+
+static Window
+get_wm_window (GdkDisplay *display)
+{
+ Display *xdisplay;
+ Atom wm_check;
+ int result;
+ Atom actual_type;
+ int actual_format;
+ unsigned long n_items;
+ unsigned long bytes_after;
+ unsigned char *prop;
+ Window wm_window;
+
+ xdisplay = gdk_x11_display_get_xdisplay (display);
+ wm_check = XInternAtom (xdisplay, "_NET_SUPPORTING_WM_CHECK", False);
+
+ gdk_x11_display_error_trap_push (display);
+
+ result = XGetWindowProperty (xdisplay,
+ XDefaultRootWindow (xdisplay),
+ wm_check,
+ 0,
+ G_MAXLONG,
+ False,
+ XA_WINDOW,
+ &actual_type,
+ &actual_format,
+ &n_items,
+ &bytes_after,
+ &prop);
+
+ gdk_x11_display_error_trap_pop_ignored (display);
+
+ if (result != Success ||
+ actual_type != XA_WINDOW ||
+ n_items == 0)
+ {
+ XFree (prop);
+ return None;
+ }
+
+ wm_window = *(Window *) prop;
+ XFree (prop);
+
+ return wm_window;
+}
+#endif
+
+static GStrv
+get_current_keybindings (void)
+{
+#ifdef GDK_WINDOWING_X11
+ GdkDisplay *display;
+ Display *xdisplay;
+ Atom keybindings_atom;
+ Window wm_window;
+ char *keybindings;
+ GStrv results;
+
+ display = gdk_display_get_default ();
+ if (!GDK_IS_X11_DISPLAY (display))
+ return NULL;
+
+ xdisplay = gdk_x11_display_get_xdisplay (display);
+ keybindings_atom = XInternAtom (xdisplay, "_GNOME_WM_KEYBINDINGS", False);
+
+ wm_window = get_wm_window (display);
+ keybindings = get_window_manager_property (display,
+ keybindings_atom,
+ wm_window);
+
+ if (keybindings != NULL)
+ {
+ GStrv p;
+
+ results = g_strsplit (keybindings, ",", -1);
+
+ for (p = results; p && *p; p++)
+ g_strstrip (*p);
+
+ g_free (keybindings);
+ }
+ else
+ {
+ Atom wm_atom;
+ char *wm_name;
+
+ wm_atom = XInternAtom (xdisplay, "_NET_WM_NAME", False);
+ wm_name = get_window_manager_property (display, wm_atom, wm_window);
+
+ results = g_new0 (char *, 2);
+ results[0] = wm_name ? wm_name : g_strdup ("Unknown");
+ }
+
+ return results;
+#else
+ return NULL;
+#endif
+}
+
+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 */
+ wm_keybindings = get_current_keybindings ();
+
+ if (wm_keybindings == NULL)
+ 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
+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_object (&self->sections_store);
+
+ 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);
+}
+
+
+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_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_item_remove_key_combo (collision, combo);
+ }
+
+ /* 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..5de19c0
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-manager.h
@@ -0,0 +1,54 @@
+/*
+ * 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_reset_shortcut (CcKeyboardManager *self,
+ CcKeyboardItem *item);
+
+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..2e91fe7
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-panel.c
@@ -0,0 +1,266 @@
+/* cc-keyboard-panel.c
+ *
+ * Copyright (C) 2010 Intel, Inc
+ * Copyright (C) 2016 Endless, Inc
+ * Copyright (C) 2020 System76, 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>
+ * Ian Douglas Scott <idscott@system76.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <glib/gi18n.h>
+
+#include "cc-keyboard-panel.h"
+#include "cc-keyboard-resources.h"
+#include "cc-keyboard-shortcut-dialog.h"
+#include "cc-input-list-box.h"
+#include "cc-xkb-modifier-dialog.h"
+
+#include "keyboard-shortcuts.h"
+
+struct _CcKeyboardPanel
+{
+ CcPanel parent_instance;
+
+ GtkCheckButton *per_window_source;
+ GtkCheckButton *same_source;
+ GSettings *keybindings_settings;
+
+ GSettings *input_source_settings;
+ AdwPreferencesGroup *input_switch_group;
+ AdwActionRow *alt_chars_row;
+ AdwActionRow *compose_row;
+ GtkWidget *value_alternate_chars;
+ GtkWidget *value_compose;
+
+ AdwActionRow *common_shortcuts_row;
+};
+
+CC_PANEL_REGISTER (CcKeyboardPanel, cc_keyboard_panel)
+
+enum {
+ PROP_0,
+ PROP_PARAMETERS
+};
+
+static const CcXkbModifier LV3_MODIFIER = {
+ "lv3:",
+ N_("Alternate Characters Key"),
+ N_("The alternate characters key can be used to enter additional characters. These are sometimes printed as a third-option on your keyboard."),
+ (CcXkbOption[]){
+ { NC_("keyboard key", "Left Alt"), "lv3:lalt_switch" },
+ { NC_("keyboard key", "Right Alt"), "lv3:ralt_switch" },
+ { NC_("keyboard key", "Left Super"), "lv3:lwin_switch" },
+ { NC_("keyboard key", "Right Super"), "lv3:rwin_switch" },
+ { NC_("keyboard key", "Menu key"), "lv3:menu_switch" },
+ { NC_("keyboard key", "Right Ctrl"), "lv3:switch" },
+ { NULL, NULL }
+ },
+ "lv3:ralt_switch",
+};
+
+static const CcXkbModifier COMPOSE_MODIFIER = {
+ "compose:",
+ N_("Compose Key"),
+ N_("The compose key allows a wide variety of characters to be entered. To use it, press compose then a sequence of characters. "
+ " For example, compose key followed by <b>C</b> and <b>o</b> will enter <b>©</b>, "
+ "<b>a</b> followed by <b>'</b> will enter <b>á</b>."),
+ (CcXkbOption[]){
+ { NC_("keyboard key", "Left Alt"), "compose:lalt" },
+ { NC_("keyboard key", "Right Alt"), "compose:ralt" },
+ { NC_("keyboard key", "Left Super"), "compose:lwin" },
+ { NC_("keyboard key", "Right Super"), "compose:rwin" },
+ { NC_("keyboard key", "Menu key"), "compose:menu" },
+ { NC_("keyboard key", "Right Ctrl"), "compose:rctrl" },
+ { NC_("keyboard key", "Caps Lock"), "compose:caps" },
+ { NC_("keyboard key", "Scroll Lock"), "compose:sclk" },
+ { NC_("keyboard key", "Print Screen"), "compose:prsc" },
+ { NULL, NULL }
+ },
+ NULL,
+};
+
+static void
+special_chars_activated (AdwActionRow *row,
+ CcKeyboardPanel *self)
+{
+ const CcXkbModifier *modifier;
+ GtkWindow *window, *dialog;
+
+ window = GTK_WINDOW (cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (self))));
+
+ if (row == self->alt_chars_row)
+ modifier = &LV3_MODIFIER;
+ else if (row == self->compose_row)
+ modifier = &COMPOSE_MODIFIER;
+ else
+ return;
+
+ dialog = GTK_WINDOW (cc_xkb_modifier_dialog_new (self->input_source_settings, modifier));
+
+ gtk_window_set_transient_for (dialog, window);
+ gtk_widget_show (GTK_WIDGET (dialog));
+}
+
+static void
+keyboard_shortcuts_activated (AdwActionRow *row,
+ CcKeyboardPanel *self)
+{
+ GtkWindow *window;
+ GtkWidget *shortcut_dialog;
+
+ if (row == self->common_shortcuts_row)
+ {
+ window = GTK_WINDOW (cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (self))));
+
+ shortcut_dialog = cc_keyboard_shortcut_dialog_new ();
+ gtk_window_set_transient_for (GTK_WINDOW (shortcut_dialog), window);
+ gtk_widget_show (GTK_WIDGET (shortcut_dialog));
+ }
+}
+
+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);
+
+ g_clear_object (&self->input_source_settings);
+ g_clear_object (&self->keybindings_settings);
+
+ G_OBJECT_CLASS (cc_keyboard_panel_parent_class)->finalize (object);
+}
+
+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;
+
+ g_object_class_override_property (object_class, PROP_PARAMETERS, "parameters");
+
+ g_type_ensure (CC_TYPE_INPUT_LIST_BOX);
+
+ 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, input_switch_group);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, per_window_source);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, same_source);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, alt_chars_row);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, compose_row);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, value_alternate_chars);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, value_compose);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, common_shortcuts_row);
+
+ gtk_widget_class_bind_template_callback (widget_class, special_chars_activated);
+ gtk_widget_class_bind_template_callback (widget_class, keyboard_shortcuts_activated);
+}
+
+static gboolean
+translate_switch_input_source (GValue *value,
+ GVariant *variant,
+ gpointer user_data)
+{
+ g_autofree const gchar **strv = NULL;
+ g_autofree gchar *accel_text = NULL;
+ g_autofree gchar *label = NULL;
+ CcKeyCombo combo = { 0 };
+
+ strv = g_variant_get_strv (variant, NULL);
+
+ gtk_accelerator_parse (strv[0] ? strv[0] : "", &combo.keyval, &combo.mask);
+ accel_text = convert_keysym_state_to_string (&combo);
+
+ label = g_strdup_printf (_("Input sources can be switched using the %s "
+ "keyboard shortcut.\nThis can be changed in "
+ "the keyboard shortcut settings."),
+ accel_text);
+
+ g_value_set_string (value, label);
+
+ return TRUE;
+}
+
+static void
+cc_keyboard_panel_init (CcKeyboardPanel *self)
+{
+ g_resources_register (cc_keyboard_get_resource ());
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->input_source_settings = g_settings_new ("org.gnome.desktop.input-sources");
+
+ /* "Input Source Switching" section */
+ g_settings_bind (self->input_source_settings, "per-window",
+ self->same_source, "active",
+ G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN);
+ self->keybindings_settings = g_settings_new ("org.gnome.desktop.wm.keybindings");
+ g_settings_bind_with_mapping (self->keybindings_settings, "switch-input-source",
+ self->input_switch_group, "description",
+ G_SETTINGS_BIND_GET,
+ translate_switch_input_source,
+ NULL, NULL, NULL);
+
+ /* "Type Special Characters" section */
+ g_settings_bind_with_mapping (self->input_source_settings,
+ "xkb-options",
+ self->value_alternate_chars,
+ "label",
+ G_SETTINGS_BIND_GET,
+ xcb_modifier_transform_binding_to_label,
+ NULL,
+ (gpointer)&LV3_MODIFIER,
+ NULL);
+ g_settings_bind_with_mapping (self->input_source_settings,
+ "xkb-options",
+ self->value_compose,
+ "label",
+ G_SETTINGS_BIND_GET,
+ xcb_modifier_transform_binding_to_label,
+ NULL,
+ (gpointer)&COMPOSE_MODIFIER,
+ NULL);
+}
diff --git a/panels/keyboard/cc-keyboard-panel.h b/panels/keyboard/cc-keyboard-panel.h
new file mode 100644
index 0000000..3d3076b
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-panel.h
@@ -0,0 +1,32 @@
+/*
+ * 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)
+
+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..4975a40
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-panel.ui
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <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="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <child type="content">
+ <object class="AdwPreferencesPage">
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Input Sources</property>
+ <property name="description" translatable="yes">Includes keyboard layouts and input methods.</property>
+ <child>
+ <object class="CcInputListBox" id="input_list">
+ <property name="vexpand">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup" id="input_switch_group">
+ <property name="title" translatable="yes">Input Source Switching</property>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Use the _same source for all windows</property>
+ <property name="use-underline">True</property>
+ <property name="activatable-widget">same_source</property>
+ <child type="prefix">
+ <object class="GtkCheckButton" id="same_source">
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow">
+ <property name="title" translatable="yes">Switch input sources _individually for each window</property>
+ <property name="use-underline">True</property>
+ <property name="activatable-widget">per_window_source</property>
+ <child type="prefix">
+ <object class="GtkCheckButton" id="per_window_source">
+ <property name="active" bind-source="same_source" bind-flags="sync-create|invert-boolean"/>
+ <property name="valign">center</property>
+ <property name="group">same_source</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Special Character Entry</property>
+ <property name="description" translatable="yes">Methods for entering symbols and letter variants using the keyboard.</property>
+ <child>
+ <object class="AdwActionRow" id="alt_chars_row">
+ <property name="title" translatable="yes">Alternate Characters Key</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="special_chars_activated" object="CcKeyboardPanel" swapped="no" />
+ <child>
+ <object class="GtkLabel" id="value_alternate_chars">
+ <property name="label" translatable="no">Right Alt</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="icon_name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwActionRow" id="compose_row">
+ <property name="title" translatable="yes">Compose Key</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="special_chars_activated" object="CcKeyboardPanel" swapped="no" />
+ <child>
+ <object class="GtkLabel" id="value_compose">
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="icon_name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <property name="title" translatable="yes">Keyboard Shortcuts</property>
+ <child>
+ <object class="AdwActionRow" id="common_shortcuts_row">
+ <property name="title" translatable="yes">View and Customize Shortcuts</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="keyboard_shortcuts_activated" object="CcKeyboardPanel" swapped="no" />
+ <child>
+ <object class="GtkImage">
+ <property name="icon_name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/keyboard/cc-keyboard-shortcut-dialog.c b/panels/keyboard/cc-keyboard-shortcut-dialog.c
new file mode 100644
index 0000000..15f3dfd
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-dialog.c
@@ -0,0 +1,877 @@
+/* cc-keyboard-shortcut-dialog.c
+ *
+ * Copyright (C) 2010 Intel, Inc
+ * Copyright (C) 2016 Endless, Inc
+ * Copyright (C) 2020 System76, 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>
+ * Ian Douglas Scott <idscott@system76.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <config.h>
+#include <glib/gi18n.h>
+#include <adwaita.h>
+
+#include "cc-keyboard-shortcut-dialog.h"
+#include "cc-keyboard-item.h"
+#include "cc-keyboard-manager.h"
+#include "cc-keyboard-shortcut-editor.h"
+#include "cc-keyboard-shortcut-row.h"
+#include "cc-list-row.h"
+#include "cc-util.h"
+#include "keyboard-shortcuts.h"
+
+#define SHORTCUT_DELIMITERS "+ "
+
+typedef struct {
+ gchar *section_title;
+ gchar *section_id;
+ guint modified_count;
+ GtkLabel *modified_label;
+} SectionRowData;
+
+typedef struct {
+ CcKeyboardItem *item;
+ gchar *section_title;
+ gchar *section_id;
+ SectionRowData *section_data;
+} ShortcutRowData;
+
+struct _CcKeyboardShortcutDialog
+{
+ GtkDialog parent_instance;
+
+ GtkSizeGroup *accelerator_sizegroup;
+ GtkWidget *back_button;
+ GtkListBoxRow *custom_shortcut_add_row;
+ guint custom_shortcut_count;
+ GtkWidget *empty_custom_shortcuts_placeholder;
+ GtkWidget *empty_search_placeholder;
+ GtkHeaderBar *headerbar;
+ GtkStack *header_stack;
+ GtkWidget *reset_all_button;
+ GtkWidget *section_box;
+ GtkSearchEntry *search_entry;
+ GtkListBox *section_listbox;
+ GtkListBoxRow *section_row;
+ GtkWidget *shortcut_box;
+ GtkListBox *shortcut_listbox;
+ GtkStack *stack;
+
+ CcKeyboardManager *manager;
+ GtkWidget *shortcut_editor;
+ GHashTable *sections;
+ };
+
+G_DEFINE_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, GTK_TYPE_DIALOG)
+static gboolean
+is_matched_shortcut_present (GtkListBox *listbox,
+ gpointer user_data);
+
+static SectionRowData*
+section_row_data_new (const gchar *section_id,
+ const gchar *section_title,
+ GtkLabel *modified_label)
+{
+ SectionRowData *data;
+
+ data = g_new0 (SectionRowData, 1);
+ data->section_id = g_strdup (section_id);
+ data->section_title = g_strdup (section_title);
+ data->modified_count = 0;
+ data->modified_label = modified_label;
+
+ return data;
+}
+
+static void
+section_row_data_free (SectionRowData *data)
+{
+ g_free (data->section_id);
+ g_free (data->section_title);
+ g_free (data);
+}
+
+static ShortcutRowData*
+shortcut_row_data_new (CcKeyboardItem *item,
+ const gchar *section_id,
+ const gchar *section_title,
+ SectionRowData *section_data)
+{
+ ShortcutRowData *data;
+
+ data = g_new0 (ShortcutRowData, 1);
+ data->item = g_object_ref (item);
+ data->section_id = g_strdup (section_id);
+ data->section_title = g_strdup (section_title);
+ data->section_data = section_data;
+
+ return data;
+}
+
+static void
+shortcut_row_data_free (ShortcutRowData *data)
+{
+ g_object_unref (data->item);
+ g_free (data->section_id);
+ g_free (data->section_title);
+ g_free (data);
+}
+
+static GtkListBoxRow*
+add_section (CcKeyboardShortcutDialog *self,
+ const gchar *section_id,
+ const gchar *section_title)
+{
+ GtkWidget *icon, *modified_label, *row;
+
+ icon = gtk_image_new_from_icon_name ("go-next-symbolic");
+ gtk_widget_add_css_class (icon, "dim-label");
+
+ modified_label = gtk_label_new (NULL);
+ gtk_widget_add_css_class (modified_label, "dim-label");
+
+ row = adw_action_row_new ();
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE);
+ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), _(section_title));
+ //TODO gtk_container_add (GTK_CONTAINER (row), modified_label);
+ //TODO gtk_container_add (GTK_CONTAINER (row), icon);
+
+ g_object_set_data_full (G_OBJECT (row),
+ "data",
+ section_row_data_new (section_id, section_title, GTK_LABEL (modified_label)),
+ (GDestroyNotify)section_row_data_free);
+
+ g_hash_table_insert (self->sections, g_strdup (section_id), row);
+ gtk_list_box_append (self->section_listbox, row);
+
+ return GTK_LIST_BOX_ROW (row);
+}
+
+static void
+set_custom_shortcut_placeholder_visibility (CcKeyboardShortcutDialog *self)
+{
+ SectionRowData *section_data;
+ gboolean is_custom_shortcuts = FALSE;
+
+ if (self->section_row != NULL)
+ {
+ section_data = g_object_get_data (G_OBJECT (self->section_row), "data");
+ is_custom_shortcuts = (strcmp (section_data->section_id, "custom") == 0);
+
+ gtk_stack_set_transition_type (self->stack, GTK_STACK_TRANSITION_TYPE_CROSSFADE);
+ if (is_custom_shortcuts && (self->custom_shortcut_count == 0))
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->empty_custom_shortcuts_placeholder));
+ else
+ gtk_stack_set_visible_child (self->stack, self->shortcut_box);
+ }
+}
+
+static void
+add_item (CcKeyboardShortcutDialog *self,
+ CcKeyboardItem *item,
+ const gchar *section_id,
+ const gchar *section_title)
+{
+ GtkWidget *row;
+ GtkListBoxRow *section_row;
+ SectionRowData *section_data;
+
+ section_row = g_hash_table_lookup (self->sections, section_id);
+ if (section_row == NULL)
+ section_row = add_section (self, section_id, section_title);
+
+ section_data = g_object_get_data (G_OBJECT (section_row), "data");
+
+ row = GTK_WIDGET (cc_keyboard_shortcut_row_new (item,
+ self->manager,
+ CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
+ self->accelerator_sizegroup));
+
+ g_object_set_data_full (G_OBJECT (row),
+ "data",
+ shortcut_row_data_new (item, section_id, section_title, section_data),
+ (GDestroyNotify)shortcut_row_data_free);
+
+ if (strcmp (section_id, "custom") == 0)
+ {
+ self->custom_shortcut_count++;
+ set_custom_shortcut_placeholder_visibility (self);
+ }
+
+ gtk_list_box_append (self->shortcut_listbox, row);
+}
+
+static void
+remove_item (CcKeyboardShortcutDialog *self,
+ CcKeyboardItem *item)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->shortcut_listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ ShortcutRowData *row_data;
+
+ if (!GTK_IS_LIST_BOX_ROW (child))
+ continue;
+
+ row_data = g_object_get_data (G_OBJECT (child), "data");
+
+ if (row_data && row_data->item == item)
+ {
+ if (strcmp (row_data->section_id, "custom") == 0)
+ {
+ self->custom_shortcut_count--;
+ set_custom_shortcut_placeholder_visibility (self);
+ }
+
+ gtk_list_box_remove (self->shortcut_listbox, child);
+ break;
+ }
+ }
+}
+
+static void
+update_modified_counts (CcKeyboardShortcutDialog *self)
+{
+ SectionRowData *section_data;
+ ShortcutRowData *shortcut_data;
+ g_autofree gchar *modified_text = NULL;
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->section_listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (!GTK_IS_LIST_BOX_ROW (child))
+ continue;
+
+ section_data = g_object_get_data (G_OBJECT (child), "data");
+ section_data->modified_count = 0;
+ }
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->shortcut_listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (!GTK_IS_LIST_BOX_ROW (child))
+ continue;
+
+ if (GTK_LIST_BOX_ROW (child) == self->custom_shortcut_add_row)
+ continue;
+ shortcut_data = g_object_get_data (G_OBJECT (child), "data");
+ if (!cc_keyboard_item_is_value_default (shortcut_data->item))
+ shortcut_data->section_data->modified_count++;
+ }
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->section_listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (!GTK_IS_LIST_BOX_ROW (child))
+ continue;
+
+ section_data = g_object_get_data (G_OBJECT (child), "data");
+ if (section_data->modified_count > 0)
+ {
+ modified_text = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE,
+ "%d modified",
+ "%d modified",
+ section_data->modified_count),
+ section_data->modified_count);
+ gtk_label_set_text (section_data->modified_label, modified_text);
+ }
+ else
+ {
+ gtk_label_set_text (section_data->modified_label, "");
+ }
+ }
+}
+
+static void
+show_section_list (CcKeyboardShortcutDialog *self)
+{
+ if (self->section_row != NULL)
+ gtk_stack_set_transition_type (self->stack, GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT);
+ else
+ gtk_stack_set_transition_type (self->stack, GTK_STACK_TRANSITION_TYPE_NONE);
+ self->section_row = NULL;
+
+ gtk_stack_set_visible_child (self->stack, self->section_box);
+ gtk_window_set_title (GTK_WINDOW (self), _("Keyboard Shortcuts"));
+ gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
+ gtk_stack_set_visible_child (self->header_stack, self->reset_all_button);
+ gtk_widget_set_visible (GTK_WIDGET (self->search_entry), TRUE);
+
+ update_modified_counts (self);
+}
+
+static void
+show_shortcut_list (CcKeyboardShortcutDialog *self)
+{
+ SectionRowData *section_data;
+ gchar *title;
+ gboolean is_custom_shortcuts = FALSE;
+
+ title = _("Keyboard Shortcuts");
+ gtk_stack_set_transition_type (self->stack, GTK_STACK_TRANSITION_TYPE_NONE);
+ if (self->section_row != NULL)
+ {
+ section_data = g_object_get_data (G_OBJECT (self->section_row), "data");
+ title = _(section_data->section_title);
+ is_custom_shortcuts = (strcmp (section_data->section_id, "custom") == 0);
+ gtk_stack_set_transition_type (self->stack, GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT);
+ }
+ gtk_list_box_invalidate_filter (self->shortcut_listbox);
+
+ if (is_custom_shortcuts && (self->custom_shortcut_count == 0))
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->empty_custom_shortcuts_placeholder));
+ else
+ gtk_stack_set_visible_child (self->stack, self->shortcut_box);
+
+ gtk_window_set_title (GTK_WINDOW (self), title);
+ set_custom_shortcut_placeholder_visibility (self);
+ gtk_stack_set_visible_child (self->header_stack, self->back_button);
+ gtk_widget_set_visible (GTK_WIDGET (self->search_entry), self->section_row == NULL);
+
+}
+
+static void
+add_custom_shortcut_clicked_cb (CcKeyboardShortcutDialog *self)
+{
+ CcKeyboardShortcutEditor *editor;
+
+ editor = CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor);
+
+ 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
+section_row_activated (GtkWidget *button,
+ GtkListBoxRow *row,
+ CcKeyboardShortcutDialog *self)
+{
+ self->section_row = row;
+ show_shortcut_list (self);
+}
+
+static void
+shortcut_row_activated (GtkWidget *button,
+ GtkListBoxRow *row,
+ CcKeyboardShortcutDialog *self)
+{
+ CcKeyboardShortcutEditor *editor;
+
+ if (row == self->custom_shortcut_add_row)
+ {
+ add_custom_shortcut_clicked_cb (self);
+ return;
+ }
+
+ editor = CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor);
+
+ ShortcutRowData *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);
+
+ gtk_widget_show (self->shortcut_editor);
+}
+
+static void
+back_button_clicked_cb (CcKeyboardShortcutDialog *self)
+{
+ show_section_list (self);
+}
+
+static void
+reset_shortcut (CcKeyboardShortcutDialog *self,
+ GtkWidget *row)
+{
+ ShortcutRowData *data;
+
+ if (row == GTK_WIDGET (self->custom_shortcut_add_row))
+ return;
+
+ data = g_object_get_data (G_OBJECT (row), "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
+on_reset_all_dialog_response_cb (GtkDialog *dialog,
+ gint response,
+ CcKeyboardShortcutDialog *self)
+{
+
+ if (response == GTK_RESPONSE_ACCEPT)
+ {
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (GTK_WIDGET (self->shortcut_listbox));
+ child;
+ child = gtk_widget_get_next_sibling (child))
+ {
+ if (!GTK_IS_LIST_BOX_ROW (child))
+ continue;
+ if (GTK_LIST_BOX_ROW (child) == self->custom_shortcut_add_row)
+ continue;
+ reset_shortcut (self, child);
+ }
+ }
+
+ gtk_window_destroy (GTK_WINDOW (dialog));
+ update_modified_counts (self);
+}
+
+static void
+reset_all_clicked_cb (CcKeyboardShortcutDialog *self)
+{
+ GtkWidget *dialog, *button;
+
+ dialog = gtk_message_dialog_new (GTK_WINDOW (self),
+ 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_widget_add_css_class (button, "destructive-action");
+
+ g_signal_connect (dialog, "response", G_CALLBACK (on_reset_all_dialog_response_cb), self);
+
+ gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+search_entry_cb (CcKeyboardShortcutDialog *self)
+{
+ gboolean is_shortcut = is_matched_shortcut_present (self->shortcut_listbox, self);
+ const gchar *search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
+
+ if (!is_shortcut)
+ gtk_stack_set_visible_child (self->stack, self->empty_search_placeholder);
+ else if (g_utf8_strlen (search_text, -1) == 0 && self->section_row == NULL)
+ show_section_list (self);
+ else if (gtk_stack_get_visible_child (self->stack) != self->shortcut_box)
+ show_shortcut_list (self);
+ else
+ gtk_list_box_invalidate_filter (self->shortcut_listbox);
+}
+
+static gboolean
+strv_contains_prefix_or_match (gchar **strv,
+ const gchar *prefix)
+{
+ 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 (guint i = 0; strv[i]; i++)
+ {
+ if (g_str_has_prefix (strv[i], prefix))
+ return TRUE;
+ }
+
+ for (guint 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 ("gtk40", "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)
+{
+ g_auto(GStrv) shortcut_tokens = NULL, search_tokens = NULL;
+ g_autofree gchar *normalized_accel = NULL;
+ g_autofree gchar *accel = NULL;
+ gboolean match;
+ GList *key_combos;
+ CcKeyCombo *combo;
+
+ key_combos = cc_keyboard_item_get_key_combos (item);
+ for (GList *l = key_combos; l != NULL; l = l->next)
+ {
+ combo = l->data;
+
+ if (is_empty_binding (combo))
+ continue;
+
+ 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 (guint 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;
+ }
+
+ if (match)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static gint
+section_sort_function (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ SectionRowData *a_data, *b_data;
+
+ 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 (g_strcmp0 (a_data->section_id, "custom") == 0)
+ return 1;
+
+ return g_strcmp0 (a_data->section_title, b_data->section_title);
+}
+
+static gint
+shortcut_sort_function (GtkListBoxRow *a,
+ GtkListBoxRow *b,
+ gpointer user_data)
+{
+ CcKeyboardShortcutDialog *self = user_data;
+ ShortcutRowData *a_data, *b_data;
+ gint retval;
+
+ if (a == self->custom_shortcut_add_row)
+ return 1;
+ else if (b == self->custom_shortcut_add_row)
+ return -1;
+
+ a_data = g_object_get_data (G_OBJECT (a), "data");
+ b_data = g_object_get_data (G_OBJECT (b), "data");
+
+ 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 gboolean
+shortcut_filter_function (GtkListBoxRow *row,
+ gpointer userdata)
+{
+ CcKeyboardShortcutDialog *self = userdata;
+ SectionRowData *section_data;
+ ShortcutRowData *data;
+ CcKeyboardItem *item;
+ gboolean retval;
+ g_autofree gchar *search = NULL;
+ g_autofree gchar *name = NULL;
+ g_auto(GStrv) terms = NULL;
+ gboolean is_custom_shortcuts = FALSE;
+ const gchar *search_text;
+
+ if (self->section_row != NULL)
+ {
+ section_data = g_object_get_data (G_OBJECT (self->section_row), "data");
+ is_custom_shortcuts = (strcmp (section_data->section_id, "custom") == 0);
+
+ data = g_object_get_data (G_OBJECT (row), "data");
+ if (data && strcmp (data->section_id, section_data->section_id) != 0)
+ return FALSE;
+ }
+
+ if (row == self->custom_shortcut_add_row)
+ return is_custom_shortcuts;
+
+ search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
+ if (g_utf8_strlen (search_text, -1) == 0)
+ return TRUE;
+
+ 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 (search_text);
+ terms = g_strsplit (search, " ", -1);
+
+ for (guint i = 0; terms && terms[i]; i++)
+ {
+ retval = strstr (name, terms[i]) || search_match_shortcut (item, terms[i]);
+ if (!retval)
+ break;
+ }
+
+ return retval;
+}
+
+static gboolean
+is_matched_shortcut_present (GtkListBox* listbox,
+ gpointer user_data)
+{
+ for (gint i = 0; ; i++)
+ {
+ GtkListBoxRow *current = gtk_list_box_get_row_at_index (listbox, i);
+ if (!current)
+ return FALSE;
+ if (shortcut_filter_function (current, user_data))
+ return TRUE;
+ }
+}
+
+static void
+shortcut_header_function (GtkListBoxRow *row,
+ GtkListBoxRow *before,
+ gpointer user_data)
+{
+ CcKeyboardShortcutDialog *self = user_data;
+ gboolean add_header;
+ ShortcutRowData *data, *before_data;
+
+ data = g_object_get_data (G_OBJECT (row), "data");
+
+ if (row == self->custom_shortcut_add_row)
+ {
+
+ add_header = FALSE;
+ }
+ else if (before && before != self->custom_shortcut_add_row)
+ {
+ before_data = g_object_get_data (G_OBJECT (before), "data");
+ add_header = g_strcmp0 (before_data->section_id, data->section_id) != 0;
+ }
+ else
+ {
+ add_header = TRUE;
+ }
+
+ if (self->section_row != NULL)
+ add_header = FALSE;
+
+ if (add_header)
+ {
+ GtkWidget *box, *label, *separator;
+ g_autofree gchar *markup = NULL;
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+ if (!before)
+ gtk_widget_set_margin_top (box, 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_box_append (GTK_BOX (box), label);
+
+ separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_append (GTK_BOX (box), separator);
+
+ gtk_list_box_row_set_header (row, box);
+ }
+ else
+ {
+ gtk_list_box_row_set_header (row, NULL);
+ }
+}
+
+static void
+cc_keyboard_shortcut_dialog_constructed (GObject *object)
+{
+ CcKeyboardShortcutDialog *self = CC_KEYBOARD_SHORTCUT_DIALOG (object);
+
+ G_OBJECT_CLASS (cc_keyboard_shortcut_dialog_parent_class)->constructed (object);
+
+ /* Setup the dialog's transient parent */
+ gtk_window_set_transient_for (GTK_WINDOW (self->shortcut_editor), GTK_WINDOW (self));
+}
+
+static void
+cc_keyboard_shortcut_dialog_finalize (GObject *object)
+{
+ CcKeyboardShortcutDialog *self = CC_KEYBOARD_SHORTCUT_DIALOG (object);
+
+ g_clear_object (&self->manager);
+ g_clear_pointer (&self->sections, g_hash_table_destroy);
+ g_clear_pointer ((GtkWindow**)&self->shortcut_editor, gtk_window_destroy);
+}
+
+static void
+cc_keyboard_shortcut_dialog_class_init (CcKeyboardShortcutDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructed = cc_keyboard_shortcut_dialog_constructed;
+ object_class->finalize = cc_keyboard_shortcut_dialog_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-keyboard-shortcut-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, accelerator_sizegroup);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, back_button);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, custom_shortcut_add_row);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_custom_shortcuts_placeholder);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_search_placeholder);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, headerbar);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, header_stack);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, reset_all_button);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, search_entry);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_box);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_box);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, stack);
+
+ gtk_widget_class_bind_template_callback (widget_class, add_custom_shortcut_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, back_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, reset_all_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_entry_cb);
+ gtk_widget_class_bind_template_callback (widget_class, section_row_activated);
+ gtk_widget_class_bind_template_callback (widget_class, shortcut_row_activated);
+}
+
+static void
+cc_keyboard_shortcut_dialog_init (CcKeyboardShortcutDialog *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_search_entry_set_key_capture_widget (self->search_entry, GTK_WIDGET (self));
+
+ self->manager = cc_keyboard_manager_new ();
+
+ self->shortcut_editor = cc_keyboard_shortcut_editor_new (self->manager);
+
+ self->sections = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ self->section_row = NULL;
+
+ 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);
+
+ add_section(self, "custom", "Custom Shortcuts");
+ cc_keyboard_manager_load_shortcuts (self->manager);
+
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->section_listbox),
+ section_sort_function,
+ self,
+ NULL);
+
+ gtk_list_box_set_filter_func (self->shortcut_listbox,
+ shortcut_filter_function,
+ self,
+ NULL);
+ gtk_list_box_set_header_func (self->shortcut_listbox,
+ shortcut_header_function,
+ self,
+ NULL);
+ gtk_list_box_set_sort_func (GTK_LIST_BOX (self->shortcut_listbox),
+ shortcut_sort_function,
+ self,
+ NULL);
+
+ show_section_list (self);
+}
+
+GtkWidget*
+cc_keyboard_shortcut_dialog_new (void)
+{
+ return g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_DIALOG,
+ "use-header-bar", 1,
+ NULL);
+}
diff --git a/panels/keyboard/cc-keyboard-shortcut-dialog.h b/panels/keyboard/cc-keyboard-shortcut-dialog.h
new file mode 100644
index 0000000..4493dc2
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-dialog.h
@@ -0,0 +1,35 @@
+/* cc-keyboard-shortcut-dialog.h
+ *
+ * Copyright (C) 2020 System76, 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: Ian Douglas Scott <idscott@system76.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_KEYBOARD_SHORTCUT_DIALOG (cc_keyboard_shortcut_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, CC, KEYBOARD_SHORTCUT_DIALOG, GtkDialog)
+
+GtkWidget* cc_keyboard_shortcut_dialog_new (void);
+
+G_END_DECLS \ No newline at end of file
diff --git a/panels/keyboard/cc-keyboard-shortcut-dialog.ui b/panels/keyboard/cc-keyboard-shortcut-dialog.ui
new file mode 100644
index 0000000..d1c8068
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-dialog.ui
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcKeyboardShortcutDialog" parent="GtkDialog">
+ <property name="modal">True</property>
+ <property name="default-width">600</property>
+
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="headerbar">
+ <property name="show_title_buttons">True</property>
+ <child>
+ <object class="GtkStack" id="header_stack">
+ <property name="hhomogeneous">False</property>
+ <property name="vhomogeneous">False</property>
+ <property name="transition-type">crossfade</property>
+
+ <child>
+ <object class="GtkButton" id="back_button">
+ <property name="halign">start</property>
+ <property name="valign">center</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="back_button_clicked_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
+ <style>
+ <class name="image-button"/>
+ </style>
+ <accessibility>
+ <property name="label">Back</property>
+ </accessibility>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkButton" id="reset_all_button">
+ <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="CcKeyboardShortcutDialog" swapped="yes" />
+ </object>
+ </child>
+
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkScrolledWindow">
+ <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">450</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="margin-top">12</property>
+ <property name="width_chars">30</property>
+ <property name="halign">center</property>
+ <accessibility>
+ <property name="label" translatable="yes">Search</property>
+ </accessibility>
+ <signal name="notify::text" handler="search_entry_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="hhomogeneous">False</property>
+ <property name="vhomogeneous">False</property>
+ <child>
+ <object class="GtkBox" id="section_box">
+ <property name="orientation">vertical</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <child>
+ <object class="GtkListBox" id="section_listbox">
+ <property name="selection-mode">none</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <accessibility>
+ <property name="label" translatable="yes">Section</property>
+ </accessibility>
+ <signal name="row-activated" handler="section_row_activated" object="CcKeyboardShortcutDialog" swapped="no" />
+ <style>
+ <class name="boxed-list" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="shortcut_box">
+ <property name="orientation">vertical</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkListBox" id="shortcut_listbox">
+ <property name="selection-mode">none</property>
+ <accessibility>
+ <property name="label" translatable="yes">Shortcuts</property>
+ </accessibility>
+ <style>
+ <class name="boxed-list" />
+ </style>
+ <signal name="row-activated" handler="shortcut_row_activated" object="CcKeyboardShortcutDialog" swapped="no" />
+ <child>
+ <object class="GtkListBoxRow" id="custom_shortcut_add_row">
+ <accessibility>
+ <property name="label" translatable="yes">Add a shortcut</property>
+ </accessibility>
+ <child>
+ <object class="GtkImage">
+ <property name="hexpand">True</property>
+ <property name="icon_name">list-add-symbolic</property>
+ <property name="height_request">48</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="empty_custom_shortcuts_placeholder">
+ <property name="orientation">vertical</property>
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="spacing">18</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="background"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">input-keyboard-symbolic</property>
+ <property name="pixel-size">128</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Add Custom Shortcuts</property>
+ <attributes>
+ <attribute name="weight" value="bold" />
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Set up custom shortcuts for launching apps, running scripts, and more.</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="halign">center</property>
+ <property name="label" translatable="yes">Add Shortcut</property>
+ <style>
+ <class name="suggested-action" />
+ </style>
+ <signal name="clicked" handler="add_custom_shortcut_clicked_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="empty_search_placeholder">
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage">
+ <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="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="label" translatable="yes">Try a different search</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ </template>
+ <object class="GtkSizeGroup" id="accelerator_sizegroup" />
+</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..66cf024
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-editor.c
@@ -0,0 +1,985 @@
+/* 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 */
+ gboolean system_shortcuts_inhibited;
+ 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 = self->custom_combo;
+
+ cc_keyboard_item_disable (item);
+
+ if (combo->keycode != 0 || combo->keyval != 0 || combo->mask != 0)
+ cc_keyboard_item_add_key_combo (item, combo);
+ }
+
+ /* 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_editable_get_text (GTK_EDITABLE (self->name_entry)));
+ g_settings_set_string (cc_keyboard_item_get_settings (item),
+ "command",
+ gtk_editable_get_text (GTK_EDITABLE (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_editable_set_text (GTK_EDITABLE (self->name_entry), "");
+ gtk_editable_set_text (GTK_EDITABLE (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
+inhibit_system_shortcuts (CcKeyboardShortcutEditor *self)
+{
+ GtkNative *native;
+ GdkSurface *surface;
+
+ if (self->system_shortcuts_inhibited)
+ return;
+
+ native = gtk_widget_get_native (GTK_WIDGET (self));
+ surface = gtk_native_get_surface (native);
+
+ if (GDK_IS_TOPLEVEL (surface))
+ {
+ gdk_toplevel_inhibit_system_shortcuts (GDK_TOPLEVEL (surface), NULL);
+ self->system_shortcuts_inhibited = TRUE;
+ }
+}
+
+static void
+uninhibit_system_shortcuts (CcKeyboardShortcutEditor *self)
+{
+ GtkNative *native;
+ GdkSurface *surface;
+
+ if (!self->system_shortcuts_inhibited)
+ return;
+
+ native = gtk_widget_get_native (GTK_WIDGET (self));
+ surface = gtk_native_get_surface (native);
+
+ if (GDK_IS_TOPLEVEL (surface))
+ {
+ gdk_toplevel_restore_system_shortcuts (GDK_TOPLEVEL (surface));
+ self->system_shortcuts_inhibited = FALSE;
+ }
+}
+
+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_item_disable (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_title_buttons (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;
+ g_autofree char *accel = NULL;
+
+ 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;
+
+ uninhibit_system_shortcuts (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_item_disable (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)
+{
+ inhibit_system_shortcuts (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 = combo;
+
+ /* Headerbar */
+ gtk_window_set_title (GTK_WINDOW (self),
+ 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_editable_set_text (GTK_EDITABLE (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_editable_set_text (GTK_EDITABLE (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);
+
+ uninhibit_system_shortcuts (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_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
+on_key_pressed_cb (GtkEventControllerKey *key_controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ CcKeyboardShortcutEditor *self)
+{
+ GdkModifierType real_mask;
+ GdkEvent *event;
+ gboolean editing;
+ gboolean is_modifier;
+ guint keyval_lower;
+
+ /* 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 GDK_EVENT_PROPAGATE;
+
+ normalize_keyval_and_mask (keycode, state,
+ gtk_event_controller_key_get_group (key_controller),
+ &keyval_lower, &real_mask);
+
+ event = gtk_event_controller_get_current_event (GTK_EVENT_CONTROLLER (key_controller));
+ is_modifier = gdk_key_event_is_modifier (event);
+
+ /* A single Escape press cancels the editing */
+ if (!is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_Escape)
+ {
+ self->edited = FALSE;
+
+ uninhibit_system_shortcuts (self);
+ cancel_editing (self);
+
+ return GDK_EVENT_STOP;
+ }
+
+ /* Backspace disables the current shortcut */
+ if (!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), "");
+
+ uninhibit_system_shortcuts (self);
+
+ self->edited = FALSE;
+
+ setup_custom_shortcut (self);
+
+ return GDK_EVENT_STOP;
+ }
+
+ self->custom_is_modifier = is_modifier;
+ self->custom_combo->keycode = 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)
+ inhibit_system_shortcuts (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;
+ }
+
+ uninhibit_system_shortcuts (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;
+
+ 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, on_key_pressed_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_window_set_title (GTK_WINDOW (self), _("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..45a9ac9
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-editor.ui
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="CcKeyboardShortcutEditor" parent="GtkDialog">
+ <property name="resizable">False</property>
+ <property name="modal">True</property>
+ <property name="width_request">400</property>
+ <property name="height_request">300</property>
+ <property name="hide-on-close">True</property>
+ <signal name="close" handler="cancel_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="no" />
+ <child>
+ <object class="GtkEventControllerKey">
+ <property name="propagation-phase">capture</property>
+ <signal name="key-pressed" handler="on_key_pressed_cb" object="CcKeyboardShortcutEditor" swapped="no" />
+ </object>
+ </child>
+
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="headerbar">
+ <property name="show_title_buttons">True</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="use-underline">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="label" translatable="yes">_Remove</property>
+ <property name="use-underline">True</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 type="end">
+ <object class="GtkButton" id="add_button">
+ <property name="label" translatable="yes">_Add</property>
+ <property name="use-underline">True</property>
+ <property name="sensitive">False</property>
+ <signal name="clicked" handler="add_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkButton" id="replace_button">
+ <property name="label" translatable="yes">Re_place</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="replace_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkButton" id="set_button">
+ <property name="label" translatable="yes">_Set</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="set_button_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <property name="margin-top">12</property>
+ <property name="margin-bottom">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <child>
+ <object class="GtkLabel" id="top_info_label">
+ <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>
+ </child>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GtkBox" id="edit_box">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <child>
+ <object class="GtkPicture">
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="can-shrink">False</property>
+ <property name="file">resource:///org/gnome/control-center/keyboard/enter-keyboard-shortcut.svg</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <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="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <child>
+ <object class="GtkCenterBox">
+ <child type="center">
+ <object class="GtkShortcutLabel" id="shortcut_accel_label">
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="halign">center</property>
+ <property name="disabled-text" translatable="yes">Disabled</property>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkButton" id="reset_button">
+ <property name="icon-name">edit-clear-symbolic</property>
+ <property name="halign">end</property>
+ <accessibility>
+ <property name="label" translatable="yes">Reset</property>
+ </accessibility>
+ <signal name="clicked" handler="reset_item_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ <style>
+ <class name="flat" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="shortcut_conflict_label">
+ <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="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="label" translatable="yes">Name</property>
+ <property name="xalign">1</property>
+ <property name="mnemonic_widget">name_entry</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">0</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Command</property>
+ <property name="xalign">1</property>
+ <property name="mnemonic_widget">command_entry</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">1</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Shortcut</property>
+ <property name="xalign">1</property>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="new_shortcut_conflict_label">
+ <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>
+ <layout>
+ <property name="column">0</property>
+ <property name="row">3</property>
+ <property name="column-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="name_entry">
+ <property name="hexpand">True</property>
+ <signal name="notify::text" handler="name_entry_changed_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ <layout>
+ <property name="column">1</property>
+ <property name="row">0</property>
+ <property name="column-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="command_entry">
+ <property name="hexpand">True</property>
+ <signal name="notify::text" handler="command_entry_changed_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ <layout>
+ <property name="column">1</property>
+ <property name="row">1</property>
+ <property name="column-span">2</property>
+ </layout>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="reset_custom_button">
+ <property name="halign">end</property>
+ <property name="icon-name">edit-clear-symbolic</property>
+ <signal name="clicked" handler="reset_custom_clicked_cb" object="CcKeyboardShortcutEditor" swapped="yes" />
+ <accessibility>
+ <property name="label" translatable="yes">Reset</property>
+ </accessibility>
+ <layout>
+ <property name="column">2</property>
+ <property name="row">2</property>
+ </layout>
+ <style>
+ <class name="flat" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="custom_shortcut_stack">
+ <layout>
+ <property name="column">1</property>
+ <property name="row">2</property>
+ </layout>
+ <child>
+ <object class="GtkButton" id="change_custom_shortcut_button">
+ <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="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="disabled-text" translatable="yes">None</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </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/cc-keyboard-shortcut-row.c b/panels/keyboard/cc-keyboard-shortcut-row.c
new file mode 100644
index 0000000..8abb042
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-row.c
@@ -0,0 +1,139 @@
+/* cc-keyboard-shortcut-row.c
+ *
+ * Copyright (C) 2020 System76, 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/>.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <glib/gi18n.h>
+#include "cc-keyboard-shortcut-row.h"
+#include "keyboard-shortcuts.h"
+
+struct _CcKeyboardShortcutRow
+{
+ AdwActionRow parent_instance;
+
+ GtkLabel *accelerator_label;
+ GtkButton *reset_button;
+ GtkRevealer *reset_revealer;
+
+ CcKeyboardItem *item;
+ CcKeyboardManager *manager;
+ CcKeyboardShortcutEditor *shortcut_editor;
+};
+
+G_DEFINE_TYPE (CcKeyboardShortcutRow, cc_keyboard_shortcut_row, ADW_TYPE_ACTION_ROW)
+
+static void
+reset_shortcut_cb (CcKeyboardShortcutRow *self)
+{
+ cc_keyboard_manager_reset_shortcut (self->manager, self->item);
+}
+
+static void
+cc_keyboard_shortcut_row_class_init (CcKeyboardShortcutRowClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-keyboard-shortcut-row.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutRow, accelerator_label);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutRow, reset_button);
+ gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutRow, reset_revealer);
+
+ gtk_widget_class_bind_template_callback (widget_class, reset_shortcut_cb);
+}
+
+static void
+cc_keyboard_shortcut_row_init (CcKeyboardShortcutRow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+shortcut_modified_changed_cb (CcKeyboardShortcutRow *self)
+{
+ gtk_revealer_set_reveal_child (self->reset_revealer,
+ !cc_keyboard_item_is_value_default (self->item));
+}
+
+static gboolean
+transform_binding_to_accel (GBinding *binding,
+ const GValue *from_value,
+ GValue *to_value,
+ gpointer user_data)
+{
+ g_autoptr(CcKeyboardItem) item = NULL;
+ CcKeyCombo combo;
+ gchar *accelerator;
+
+ item = CC_KEYBOARD_ITEM (g_binding_dup_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;
+}
+
+CcKeyboardShortcutRow *
+cc_keyboard_shortcut_row_new (CcKeyboardItem *item,
+ CcKeyboardManager *manager,
+ CcKeyboardShortcutEditor *shortcut_editor,
+ GtkSizeGroup *size_group)
+{
+ CcKeyboardShortcutRow *self;
+
+ self = g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_ROW, NULL);
+ self->item = item;
+ self->manager = manager;
+ self->shortcut_editor = shortcut_editor;
+
+ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self), cc_keyboard_item_get_description (item));
+
+ g_object_bind_property_full (item,
+ "key-combos",
+ self->accelerator_label,
+ "label",
+ G_BINDING_SYNC_CREATE,
+ transform_binding_to_accel,
+ NULL, NULL, NULL);
+
+ gtk_revealer_set_reveal_child (self->reset_revealer,
+ !cc_keyboard_item_is_value_default (item));
+ g_signal_connect_object (item,
+ "notify::key-combos",
+ G_CALLBACK (shortcut_modified_changed_cb),
+ self, G_CONNECT_SWAPPED);
+
+ gtk_size_group_add_widget(size_group,
+ GTK_WIDGET (self->accelerator_label));
+
+ return self;
+}
diff --git a/panels/keyboard/cc-keyboard-shortcut-row.h b/panels/keyboard/cc-keyboard-shortcut-row.h
new file mode 100644
index 0000000..14b68ef
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-row.h
@@ -0,0 +1,38 @@
+/* cc-keyboard-shortcut-row.h
+ *
+ * Copyright (C) 2020 System76, 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/>.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+#include <adwaita.h>
+#include "cc-keyboard-item.h"
+#include "cc-keyboard-manager.h"
+#include "cc-keyboard-shortcut-editor.h"
+
+G_BEGIN_DECLS
+
+#define CC_TYPE_KEYBOARD_SHORTCUT_ROW (cc_keyboard_shortcut_row_get_type())
+G_DECLARE_FINAL_TYPE (CcKeyboardShortcutRow, cc_keyboard_shortcut_row, CC, KEYBOARD_SHORTCUT_ROW, AdwActionRow)
+
+CcKeyboardShortcutRow *cc_keyboard_shortcut_row_new (CcKeyboardItem *item,
+ CcKeyboardManager *manager,
+ CcKeyboardShortcutEditor *editor,
+ GtkSizeGroup *size_group);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-keyboard-shortcut-row.ui b/panels/keyboard/cc-keyboard-shortcut-row.ui
new file mode 100644
index 0000000..98a8a31
--- /dev/null
+++ b/panels/keyboard/cc-keyboard-shortcut-row.ui
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <!-- interface-requires gtk+ 3.0 -->
+ <template class="CcKeyboardShortcutRow" parent="AdwActionRow">
+ <property name="selectable">False</property>
+ <property name="activatable">True</property>
+ <child>
+ <object class="GtkLabel" id="accelerator_label">
+ <property name="xalign">1.0</property>
+ <property name="use-markup">True</property>
+ <style>
+ <class name="dim-label" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer" id = "reset_revealer">
+ <property name="transition-type">slide-right</property>
+ <child>
+ <object class="GtkButton" id="reset_button">
+ <property name="valign">center</property>
+ <property name="icon-name">edit-clear-symbolic</property>
+ <property name="tooltip-text" translatable="yes">Reset the shortcut to its default value</property>
+ <accessibility>
+ <property name="label" translatable="yes">Reset</property>
+ </accessibility>
+ <signal name="clicked" handler="reset_shortcut_cb" swapped="true"/>
+ <style>
+ <class name="flat" />
+ <class name="circular" />
+ <class name="reset-shortcut-button" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/panels/keyboard/cc-xkb-modifier-dialog.c b/panels/keyboard/cc-xkb-modifier-dialog.c
new file mode 100644
index 0000000..da8116e
--- /dev/null
+++ b/panels/keyboard/cc-xkb-modifier-dialog.c
@@ -0,0 +1,355 @@
+/* cc-xkb-modifier-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 <glib/gi18n.h>
+#include <adwaita.h>
+
+#include "cc-xkb-modifier-dialog.h"
+
+struct _CcXkbModifierDialog
+{
+ GtkDialog parent_instance;
+
+ GtkLabel *description_label;
+ GtkSwitch *enabled_switch;
+ GtkListBox *listbox;
+ GtkListBox *switch_listbox;
+ AdwActionRow *switch_row;
+
+ GSettings *input_source_settings;
+ const CcXkbModifier *modifier;
+ GSList *radio_group;
+};
+
+G_DEFINE_TYPE (CcXkbModifierDialog, cc_xkb_modifier_dialog, GTK_TYPE_DIALOG)
+
+static const gchar *custom_css =
+".xkb-option-button {"
+" padding: 12px;"
+"}";
+
+static const CcXkbOption*
+get_xkb_option_from_name (const CcXkbModifier *modifier, const gchar* name)
+{
+ const CcXkbOption *options = modifier->options;
+ int i;
+
+ for (i = 0; options[i].label && options[i].xkb_option; i++)
+ {
+ if (g_str_equal (name, options[i].xkb_option))
+ return &options[i];
+ }
+
+ return NULL;
+}
+
+static GtkCheckButton *
+get_radio_button_from_xkb_option_name (CcXkbModifierDialog *self,
+ const gchar *name)
+{
+ gchar *xkb_option;
+ GSList *l;
+
+ for (l = self->radio_group; l != NULL; l = l->next)
+ {
+ xkb_option = g_object_get_data (l->data, "xkb-option");
+ if (g_strcmp0 (xkb_option, name) == 0)
+ return l->data;
+ }
+
+ return NULL;
+}
+
+static void
+update_active_radio (CcXkbModifierDialog *self)
+{
+ g_auto(GStrv) options = NULL;
+ GtkCheckButton *rightalt_radio;
+ const CcXkbOption *default_option;
+ guint i;
+
+ options = g_settings_get_strv (self->input_source_settings, "xkb-options");
+
+ for (i = 0; options != NULL && options[i] != NULL; i++)
+ {
+ GtkCheckButton *radio;
+
+ if (!g_str_has_prefix (options[i], self->modifier->prefix))
+ continue;
+
+ radio = get_radio_button_from_xkb_option_name (self, options[i]);
+
+ if (!radio)
+ continue;
+
+ gtk_check_button_set_active (GTK_CHECK_BUTTON (radio), TRUE);
+ gtk_switch_set_active (self->enabled_switch, TRUE);
+ return;
+ }
+
+ if (self->modifier->default_option != NULL)
+ {
+ default_option = get_xkb_option_from_name(self->modifier, self->modifier->default_option);
+ rightalt_radio = get_radio_button_from_xkb_option_name (self, default_option->xkb_option);
+ gtk_check_button_set_active (GTK_CHECK_BUTTON (rightalt_radio), TRUE);
+ gtk_switch_set_active (self->enabled_switch, TRUE);
+ }
+ else
+ {
+ gtk_switch_set_active (self->enabled_switch, FALSE);
+ }
+}
+
+static void
+set_xkb_option (CcXkbModifierDialog *self,
+ gchar *xkb_option)
+{
+ g_autoptr(GPtrArray) array = NULL;
+ g_auto(GStrv) options = NULL;
+ gboolean found;
+ guint i;
+
+ /* Either replace the existing "<modifier>:" 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], self->modifier->prefix))
+ {
+ if (!found && xkb_option != NULL)
+ g_ptr_array_add (array, xkb_option);
+ found = TRUE;
+ }
+ else
+ {
+ g_ptr_array_add (array, options[i]);
+ }
+ }
+
+ if (!found && xkb_option != NULL)
+ g_ptr_array_add (array, xkb_option);
+
+ g_ptr_array_add (array, NULL);
+
+ g_settings_set_strv (self->input_source_settings,
+ "xkb-options",
+ (const gchar * const *) array->pdata);
+}
+
+static void
+on_active_radio_changed_cb (CcXkbModifierDialog *self,
+ GtkCheckButton *radio)
+{
+ gchar *xkb_option;
+
+ if (!gtk_check_button_get_active (GTK_CHECK_BUTTON (radio)))
+ return;
+
+ if (!gtk_switch_get_state (self->enabled_switch))
+ return;
+
+ xkb_option = (gchar *)g_object_get_data (G_OBJECT (radio), "xkb-option");
+ set_xkb_option (self, xkb_option);
+}
+
+static void
+on_xkb_options_changed_cb (CcXkbModifierDialog *self)
+{
+ if (self->modifier == NULL)
+ update_active_radio (self);
+}
+
+static gboolean
+enable_switch_changed_cb (GtkSwitch *widget,
+ gboolean state,
+ gpointer user_data)
+{
+ CcXkbModifierDialog *self = user_data;
+ gchar *xkb_option;
+ GSList *l;
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self->listbox), state);
+
+ if (state)
+ {
+ for (l = self->radio_group; l != NULL; l = l->next)
+ {
+ if (gtk_check_button_get_active (l->data))
+ {
+ xkb_option = (gchar *)g_object_get_data (l->data, "xkb-option");
+ set_xkb_option (self, xkb_option);
+ break;
+ }
+ }
+ }
+ else
+ {
+ set_xkb_option (self, NULL);
+ }
+
+ return FALSE;
+}
+
+static void
+cc_xkb_modifier_dialog_finalize (GObject *object)
+{
+ CcXkbModifierDialog *self = (CcXkbModifierDialog *)object;
+
+ g_clear_object (&self->input_source_settings);
+
+ G_OBJECT_CLASS (cc_xkb_modifier_dialog_parent_class)->finalize (object);
+}
+
+static void
+cc_xkb_modifier_dialog_class_init (CcXkbModifierDialogClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = cc_xkb_modifier_dialog_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-xkb-modifier-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, description_label);
+ gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, enabled_switch);
+ gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, switch_listbox);
+ gtk_widget_class_bind_template_child (widget_class, CcXkbModifierDialog, switch_row);
+
+ gtk_widget_class_bind_template_callback (widget_class, enable_switch_changed_cb);
+}
+
+static void
+add_radio_buttons (CcXkbModifierDialog *self)
+{
+ g_autoptr (GSList) group = NULL;
+ GtkWidget *row, *radio_button, *last_button = NULL;
+ CcXkbOption *options = self->modifier->options;
+ int i;
+
+ for (i = 0; options[i].label && options[i].xkb_option; i++)
+ {
+ row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+ "visible", TRUE,
+ "selectable", FALSE,
+ NULL);
+ gtk_list_box_append (self->listbox, row);
+
+ radio_button = g_object_new (GTK_TYPE_CHECK_BUTTON,
+ "label", g_dpgettext2 (NULL, "keyboard key", options[i].label),
+ "group", last_button,
+ NULL);
+ gtk_widget_add_css_class (radio_button, "xkb-option-button");
+ g_object_set_data (G_OBJECT (radio_button), "xkb-option", options[i].xkb_option);
+ g_signal_connect_object (radio_button, "toggled", (GCallback)on_active_radio_changed_cb, self, G_CONNECT_SWAPPED);
+ gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), radio_button);
+
+ last_button = radio_button;
+ group = g_slist_prepend (group, radio_button);
+ }
+
+ self->radio_group = NULL;
+ if (last_button != NULL)
+ self->radio_group = g_steal_pointer (&group);
+}
+
+static void
+cc_xkb_modifier_dialog_init (CcXkbModifierDialog *self)
+{
+ g_autoptr(GtkCssProvider) provider = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_data (provider, custom_css, -1);
+
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1);
+
+ self->modifier = NULL;
+
+ 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);
+}
+
+CcXkbModifierDialog *
+cc_xkb_modifier_dialog_new (GSettings *input_settings,
+ const CcXkbModifier *modifier)
+{
+ CcXkbModifierDialog *self;
+
+ self = g_object_new (CC_TYPE_XKB_MODIFIER_DIALOG,
+ "use-header-bar", TRUE,
+ NULL);
+ self->input_source_settings = g_object_ref (input_settings);
+
+ self->modifier = modifier;
+ gtk_window_set_title (GTK_WINDOW (self), gettext (modifier->title));
+ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->switch_row), gettext (modifier->title));
+ gtk_label_set_markup (self->description_label, gettext (modifier->description));
+ gtk_widget_set_visible (GTK_WIDGET (self->switch_listbox), modifier->default_option == NULL);
+ add_radio_buttons (self);
+ update_active_radio (self);
+ gtk_widget_set_sensitive (GTK_WIDGET (self->listbox), gtk_switch_get_state (self->enabled_switch));
+
+ return self;
+}
+
+gboolean
+xcb_modifier_transform_binding_to_label (GValue *value,
+ GVariant *variant,
+ gpointer user_data)
+{
+ const CcXkbModifier *modifier = user_data;
+ const CcXkbOption *entry = NULL;
+ const char **items;
+ guint i;
+
+ items = g_variant_get_strv (variant, NULL);
+
+ for (i = 0; items != NULL && items[i] != NULL; i++)
+ {
+ entry = get_xkb_option_from_name (modifier, items[i]);
+ if (entry != NULL)
+ break;
+ }
+
+ if (entry == NULL && modifier->default_option == NULL)
+ {
+ g_value_set_string (value, _("Disabled"));
+ return TRUE;
+ }
+ else if (entry == NULL)
+ {
+ entry = get_xkb_option_from_name(modifier, modifier->default_option);
+ }
+
+ g_value_set_string (value,
+ g_dpgettext2 (NULL, "keyboard key", entry->label));
+ return TRUE;
+}
diff --git a/panels/keyboard/cc-xkb-modifier-dialog.h b/panels/keyboard/cc-xkb-modifier-dialog.h
new file mode 100644
index 0000000..91efbcd
--- /dev/null
+++ b/panels/keyboard/cc-xkb-modifier-dialog.h
@@ -0,0 +1,49 @@
+/* cc-xkb-modifier-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
+
+typedef struct
+{
+ gchar *label;
+ gchar *xkb_option;
+} CcXkbOption;
+
+typedef struct
+{
+ gchar *prefix;
+ gchar *title;
+ gchar *description;
+ CcXkbOption *options;
+ gchar *default_option;
+} CcXkbModifier;
+
+#define CC_TYPE_XKB_MODIFIER_DIALOG (cc_xkb_modifier_dialog_get_type())
+G_DECLARE_FINAL_TYPE (CcXkbModifierDialog, cc_xkb_modifier_dialog, CC, XKB_MODIFIER_DIALOG, GtkDialog)
+
+CcXkbModifierDialog *cc_xkb_modifier_dialog_new (GSettings *input_settings, const CcXkbModifier*);
+
+gboolean xcb_modifier_transform_binding_to_label (GValue*, GVariant*, gpointer);
+
+G_END_DECLS
diff --git a/panels/keyboard/cc-xkb-modifier-dialog.ui b/panels/keyboard/cc-xkb-modifier-dialog.ui
new file mode 100644
index 0000000..e5612ba
--- /dev/null
+++ b/panels/keyboard/cc-xkb-modifier-dialog.ui
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="CcXkbModifierDialog" parent="GtkDialog">
+ <property name="modal">True</property>
+ <property name="resizable">False</property>
+ <property name="default_width">500</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+ <property name="spacing">18</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkLabel" id="description_label">
+ <property name="margin_top">6</property>
+ <property name="wrap">True</property>
+ <property name="width_chars">40</property>
+ <property name="max_width_chars">40</property>
+ <property name="xalign">0</property>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkListBox" id="switch_listbox">
+ <style>
+ <class name="boxed-list"/>
+ </style>
+ <child>
+ <object class="AdwActionRow" id="switch_row">
+ <property name="activatable">False</property>
+ <property name="selectable">False</property>
+ <child>
+ <object class="GtkSwitch" id="enabled_switch">
+ <property name="valign">center</property>
+ <accessibility>
+ <property name="label" translatable="yes">Enabled</property>
+ </accessibility>
+ <signal name="state-set" handler="enable_switch_changed_cb" object="CcXkbModifierDialog" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <style>
+ <class name="boxed-list"/>
+ </style>
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ </object>
+ </child>
+
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar">
+ <property name="show-title-buttons">True</property>
+ </object>
+ </child>
+ </template>
+</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..872fcaf
--- /dev/null
+++ b/panels/keyboard/gnome-keyboard-panel.desktop.in.in
@@ -0,0 +1,18 @@
+[Desktop Entry]
+Name=Keyboard
+Comment=Change keyboard shortcuts and set your typing preferences, keyboard layouts and input sources
+Exec=gnome-control-center keyboard
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=org.gnome.Settings-keyboard-symbolic
+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/icons/meson.build b/panels/keyboard/icons/meson.build
new file mode 100644
index 0000000..384cb71
--- /dev/null
+++ b/panels/keyboard/icons/meson.build
@@ -0,0 +1,4 @@
+install_data(
+ 'scalable/org.gnome.Settings-keyboard-symbolic.svg',
+ install_dir: join_paths(control_center_icondir, 'hicolor', 'scalable', 'apps')
+)
diff --git a/panels/keyboard/icons/scalable/org.gnome.Settings-keyboard-symbolic.svg b/panels/keyboard/icons/scalable/org.gnome.Settings-keyboard-symbolic.svg
new file mode 100644
index 0000000..0f35d48
--- /dev/null
+++ b/panels/keyboard/icons/scalable/org.gnome.Settings-keyboard-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 2.5 2 c -1.367188 0 -2.5 1.132812 -2.5 2.5 v 7 c 0 1.367188 1.132812 2.5 2.5 2.5 h 11 c 1.367188 0 2.5 -1.132812 2.5 -2.5 v -7 c 0 -1.367188 -1.132812 -2.5 -2.5 -2.5 z m 0 2 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.242188 0.171875 -0.445312 0.398438 -0.488281 c 0.03125 -0.007813 0.066406 -0.011719 0.101562 -0.011719 z m 3 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 3 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 3 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m -8 3 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 3 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 3 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 3 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m -10 3 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 3 0 h 4 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -4 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 6 0 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
+</svg>
diff --git a/panels/keyboard/keyboard-shortcuts.c b/panels/keyboard/keyboard-shortcuts.c
new file mode 100644
index 0000000..cfaa11e
--- /dev/null
+++ b/panels/keyboard/keyboard-shortcuts.c
@@ -0,0 +1,450 @@
+/*
+ * 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"
+
+#define CUSTOM_KEYS_BASENAME "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings"
+
+static char *
+replace_pictures_folder (const char *description)
+{
+ g_autoptr(GRegex) pictures_regex = NULL;
+ const char *path;
+ g_autofree gchar *dirname = NULL;
+ g_autofree gchar *ret = NULL;
+
+ if (description == NULL)
+ return NULL;
+
+ if (strstr (description, "$PICTURES") == NULL)
+ return g_strdup (description);
+
+ pictures_regex = g_regex_new ("\\$PICTURES", 0, 0, NULL);
+ path = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES);
+ dirname = g_filename_display_basename (path);
+ ret = g_regex_replace (pictures_regex, description, -1,
+ 0, dirname, 0, NULL);
+
+ if (ret == NULL)
+ return g_strdup (description);
+
+ return g_steal_pointer (&ret);
+}
+
+static void
+parse_start_tag (GMarkupParseContext *ctx,
+ const gchar *element_name,
+ const gchar **attr_names,
+ const gchar **attr_values,
+ gpointer user_data,
+ GError **error)
+{
+ KeyList *keylist = (KeyList *) user_data;
+ KeyListEntry key;
+ const char *name, *schema, *description, *package, *context, *orig_description, *reverse_entry;
+ gboolean is_reversed, hidden;
+
+ name = NULL;
+ schema = NULL;
+ package = NULL;
+ context = NULL;
+
+ /* The top-level element, names the section in the tree */
+ if (g_str_equal (element_name, "KeyListEntries"))
+ {
+ const char *wm_name = NULL;
+ const char *group = NULL;
+
+ while (*attr_names && *attr_values)
+ {
+ if (g_str_equal (*attr_names, "name"))
+ {
+ if (**attr_values)
+ name = *attr_values;
+ } else if (g_str_equal (*attr_names, "group")) {
+ if (**attr_values)
+ group = *attr_values;
+ } else if (g_str_equal (*attr_names, "wm_name")) {
+ if (**attr_values)
+ wm_name = *attr_values;
+ } else if (g_str_equal (*attr_names, "schema")) {
+ if (**attr_values)
+ schema = *attr_values;
+ } else if (g_str_equal (*attr_names, "package")) {
+ if (**attr_values)
+ package = *attr_values;
+ }
+ ++attr_names;
+ ++attr_values;
+ }
+
+ if (name)
+ {
+ if (keylist->name)
+ g_warning ("Duplicate section name");
+ g_free (keylist->name);
+ keylist->name = g_strdup (name);
+ }
+ if (wm_name)
+ {
+ if (keylist->wm_name)
+ g_warning ("Duplicate window manager name");
+ g_free (keylist->wm_name);
+ keylist->wm_name = g_strdup (wm_name);
+ }
+ if (package)
+ {
+ if (keylist->package)
+ g_warning ("Duplicate gettext package name");
+ g_free (keylist->package);
+ keylist->package = g_strdup (package);
+ bind_textdomain_codeset (keylist->package, "UTF-8");
+ }
+ if (group)
+ {
+ if (keylist->group)
+ g_warning ("Duplicate group");
+ g_free (keylist->group);
+ keylist->group = g_strdup (group);
+ }
+ if (schema)
+ {
+ if (keylist->schema)
+ g_warning ("Duplicate schema");
+ g_free (keylist->schema);
+ keylist->schema = g_strdup (schema);
+ }
+ return;
+ }
+
+ if (!g_str_equal (element_name, "KeyListEntry")
+ || attr_names == NULL
+ || attr_values == NULL)
+ return;
+
+ schema = NULL;
+ description = NULL;
+ context = NULL;
+ orig_description = NULL;
+ reverse_entry = NULL;
+ is_reversed = FALSE;
+ hidden = FALSE;
+
+ while (*attr_names && *attr_values)
+ {
+ if (g_str_equal (*attr_names, "name"))
+ {
+ /* skip if empty */
+ if (**attr_values)
+ name = *attr_values;
+ } else if (g_str_equal (*attr_names, "schema")) {
+ if (**attr_values) {
+ schema = *attr_values;
+ }
+ } else if (g_str_equal (*attr_names, "description")) {
+ if (**attr_values)
+ orig_description = *attr_values;
+ } else if (g_str_equal (*attr_names, "msgctxt")) {
+ if (**attr_values)
+ context = *attr_values;
+ } else if (g_str_equal (*attr_names, "reverse-entry")) {
+ if (**attr_values)
+ reverse_entry = *attr_values;
+ } else if (g_str_equal (*attr_names, "is-reversed")) {
+ if (g_str_equal (*attr_values, "true"))
+ is_reversed = TRUE;
+ } else if (g_str_equal (*attr_names, "hidden")) {
+ if (g_str_equal (*attr_values, "true"))
+ hidden = TRUE;
+ }
+
+ ++attr_names;
+ ++attr_values;
+ }
+
+ if (name == NULL)
+ return;
+
+ if (schema == NULL &&
+ keylist->schema == NULL) {
+ g_debug ("Ignored GConf keyboard shortcut '%s'", name);
+ return;
+ }
+
+ if (context != NULL)
+ description = g_dpgettext2 (keylist->package, context, orig_description);
+ else
+ description = dgettext (keylist->package, orig_description);
+
+ key.name = g_strdup (name);
+ key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS;
+ key.description = replace_pictures_folder (description);
+ key.schema = schema ? g_strdup (schema) : g_strdup (keylist->schema);
+ key.reverse_entry = g_strdup (reverse_entry);
+ key.is_reversed = is_reversed;
+ key.hidden = hidden;
+ g_array_append_val (keylist->entries, key);
+}
+
+static const guint forbidden_keyvals[] = {
+ /* Navigation keys */
+ GDK_KEY_Home,
+ GDK_KEY_Left,
+ GDK_KEY_Up,
+ GDK_KEY_Right,
+ GDK_KEY_Down,
+ GDK_KEY_Page_Up,
+ GDK_KEY_Page_Down,
+ GDK_KEY_End,
+ GDK_KEY_Tab,
+
+ /* Return */
+ GDK_KEY_KP_Enter,
+ GDK_KEY_Return,
+
+ GDK_KEY_Mode_switch
+};
+
+static gboolean
+keyval_is_forbidden (guint keyval)
+{
+ guint i;
+
+ for (i = 0; i < G_N_ELEMENTS(forbidden_keyvals); i++) {
+ if (keyval == forbidden_keyvals[i])
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+is_valid_binding (const CcKeyCombo *combo)
+{
+ if ((combo->mask == 0 || combo->mask == GDK_SHIFT_MASK) && combo->keycode != 0)
+ {
+ guint keyval = combo->keyval;
+
+ if ((keyval >= GDK_KEY_a && keyval <= GDK_KEY_z)
+ || (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z)
+ || (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9)
+ || (keyval >= GDK_KEY_kana_fullstop && keyval <= GDK_KEY_semivoicedsound)
+ || (keyval >= GDK_KEY_Arabic_comma && keyval <= GDK_KEY_Arabic_sukun)
+ || (keyval >= GDK_KEY_Serbian_dje && keyval <= GDK_KEY_Cyrillic_HARDSIGN)
+ || (keyval >= GDK_KEY_Greek_ALPHAaccent && keyval <= GDK_KEY_Greek_omega)
+ || (keyval >= GDK_KEY_hebrew_doublelowline && keyval <= GDK_KEY_hebrew_taf)
+ || (keyval >= GDK_KEY_Thai_kokai && keyval <= GDK_KEY_Thai_lekkao)
+ || (keyval >= GDK_KEY_Hangul_Kiyeog && keyval <= GDK_KEY_Hangul_J_YeorinHieuh)
+ || (keyval == GDK_KEY_space && combo->mask == 0)
+ || keyval_is_forbidden (keyval)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+gboolean
+is_empty_binding (const CcKeyCombo *combo)
+{
+ if (combo->keyval == 0 &&
+ combo->mask == 0 &&
+ combo->keycode == 0)
+ return TRUE;
+ return FALSE;
+}
+
+gboolean
+is_valid_accel (const CcKeyCombo *combo)
+{
+ /* Unlike gtk_accelerator_valid(), we want to allow Tab when combined
+ * with some modifiers (Alt+Tab and friends)
+ */
+ return gtk_accelerator_valid (combo->keyval, combo->mask) ||
+ (combo->keyval == GDK_KEY_Tab && combo->mask != 0);
+}
+
+gchar*
+find_free_settings_path (GSettings *settings)
+{
+ g_auto(GStrv) used_names = NULL;
+ g_autofree gchar *dir = NULL;
+ int i, num, n_names;
+
+ used_names = g_settings_get_strv (settings, "custom-keybindings");
+ n_names = g_strv_length (used_names);
+
+ for (num = 0; dir == NULL; num++)
+ {
+ g_autofree gchar *tmp = NULL;
+ gboolean found = FALSE;
+
+ tmp = g_strdup_printf ("%s/custom%d/", CUSTOM_KEYS_BASENAME, num);
+ for (i = 0; i < n_names && !found; i++)
+ found = strcmp (used_names[i], tmp) == 0;
+
+ if (!found)
+ dir = g_steal_pointer (&tmp);
+ }
+
+ return g_steal_pointer (&dir);
+}
+
+KeyList*
+parse_keylist_from_file (const gchar *path)
+{
+ KeyList *keylist;
+ g_autoptr(GError) err = NULL;
+ g_autofree gchar *buf = NULL;
+ gsize buf_len;
+ guint i;
+
+ g_autoptr(GMarkupParseContext) ctx = NULL;
+ GMarkupParser parser = { parse_start_tag, NULL, NULL, NULL, NULL };
+
+ /* Parse file */
+ if (!g_file_get_contents (path, &buf, &buf_len, &err))
+ return NULL;
+
+ keylist = g_new0 (KeyList, 1);
+ keylist->entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry));
+ ctx = g_markup_parse_context_new (&parser, 0, keylist, NULL);
+
+ if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err))
+ {
+ g_warning ("Failed to parse '%s': '%s'", path, err->message);
+ g_free (keylist->name);
+ g_free (keylist->package);
+ g_free (keylist->wm_name);
+
+ for (i = 0; i < keylist->entries->len; i++)
+ g_free (((KeyListEntry *) &(keylist->entries->data[i]))->name);
+
+ g_array_free (keylist->entries, TRUE);
+ g_free (keylist);
+ return NULL;
+ }
+
+ return keylist;
+}
+
+/*
+ * Stolen from GtkCellRendererAccel:
+ * https://git.gnome.org/browse/gtk+/tree/gtk/gtkcellrendereraccel.c#n261
+ */
+gchar*
+convert_keysym_state_to_string (const CcKeyCombo *combo)
+{
+ gchar *name;
+
+ if (combo->keyval == 0 && combo->keycode == 0)
+ {
+ /* This label is displayed in a treeview cell displaying
+ * a disabled accelerator key combination.
+ */
+ name = g_strdup (_("Disabled"));
+ }
+ else
+ {
+ name = gtk_accelerator_get_label_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask);
+
+ if (name == NULL)
+ name = gtk_accelerator_name_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask);
+ }
+
+ return name;
+}
+
+/* This adjusts the keyval and modifiers such that it matches how
+ * gnome-shell detects shortcuts, which works as follows:
+ * First for the non-modifier key, the keycode that generates this
+ * keyval at the lowest shift level is determined, which might be a
+ * level > 0, such as for numbers in the num-row in AZERTY.
+ * Next it checks if all the specified modifiers were pressed.
+ */
+void
+normalize_keyval_and_mask (guint keycode,
+ GdkModifierType mask,
+ guint group,
+ guint *out_keyval,
+ GdkModifierType *out_mask)
+{
+ guint unmodified_keyval;
+ guint shifted_keyval;
+ GdkModifierType explicit_modifiers;
+ GdkModifierType used_modifiers;
+
+ /* We want shift to always be included as explicit modifier for
+ * gnome-shell shortcuts. That's because users usually think of
+ * shortcuts as including the shift key rather than being defined
+ * for the shifted keyval.
+ * This helps with num-row keys which have different keyvals on
+ * different layouts for example, but also with keys that have
+ * explicit key codes at shift level 0, that gnome-shell would prefer
+ * over shifted ones, such the DOLLAR key.
+ */
+ explicit_modifiers = gtk_accelerator_get_default_mod_mask () | GDK_SHIFT_MASK;
+ used_modifiers = mask & explicit_modifiers;
+
+ /* Find the base keyval of the pressed key without the explicit
+ * modifiers. */
+ gdk_display_translate_key (gdk_display_get_default (),
+ keycode,
+ mask & ~explicit_modifiers,
+ group,
+ &unmodified_keyval,
+ NULL,
+ NULL,
+ NULL);
+
+ /* Normalize num-row keys to the number value. This allows these
+ * shortcuts to work when switching between AZERTY and layouts where
+ * the numbers are at shift level 0. */
+ gdk_display_translate_key (gdk_display_get_default (),
+ keycode,
+ GDK_SHIFT_MASK | (mask & ~explicit_modifiers),
+ group,
+ &shifted_keyval,
+ NULL,
+ NULL,
+ NULL);
+
+ if (shifted_keyval >= GDK_KEY_0 && shifted_keyval <= GDK_KEY_9)
+ unmodified_keyval = shifted_keyval;
+
+ /* Normalise <Tab> */
+ if (unmodified_keyval == GDK_KEY_ISO_Left_Tab)
+ unmodified_keyval = GDK_KEY_Tab;
+
+ if (unmodified_keyval == GDK_KEY_Sys_Req && (used_modifiers & GDK_ALT_MASK) != 0)
+ {
+ /* HACK: we don't want to use SysRq as a keybinding (but we do
+ * want Alt+Print), so we avoid translation from Alt+Print to SysRq */
+ unmodified_keyval = GDK_KEY_Print;
+ }
+
+ *out_keyval = unmodified_keyval;
+ *out_mask = used_modifiers;
+}
diff --git a/panels/keyboard/keyboard-shortcuts.h b/panels/keyboard/keyboard-shortcuts.h
new file mode 100644
index 0000000..7c9b978
--- /dev/null
+++ b/panels/keyboard/keyboard-shortcuts.h
@@ -0,0 +1,84 @@
+/*
+ * 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 (const CcKeyCombo *combo);
+
+gboolean is_empty_binding (const CcKeyCombo *combo);
+
+gboolean is_valid_accel (const CcKeyCombo *combo);
+
+KeyList* parse_keylist_from_file (const gchar *path);
+
+gchar* convert_keysym_state_to_string (const CcKeyCombo *combo);
+
+void normalize_keyval_and_mask (guint keyval,
+ GdkModifierType mask,
+ guint group,
+ guint *out_keyval,
+ GdkModifierType *out_mask);
diff --git a/panels/keyboard/keyboard.gresource.xml b/panels/keyboard/keyboard.gresource.xml
new file mode 100644
index 0000000..a310f05
--- /dev/null
+++ b/panels/keyboard/keyboard.gresource.xml
@@ -0,0 +1,14 @@
+<?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-xkb-modifier-dialog.ui</file>
+ <file preprocess="xml-stripblanks">cc-keyboard-shortcut-row.ui</file>
+ <file preprocess="xml-stripblanks">cc-keyboard-shortcut-dialog.ui</file>
+ <file preprocess="xml-stripblanks">cc-keyboard-panel.ui</file>
+ <file preprocess="xml-stripblanks">cc-keyboard-shortcut-editor.ui</file>
+ <file preprocess="xml-stripblanks">cc-input-chooser.ui</file>
+ <file preprocess="xml-stripblanks">cc-input-row.ui</file>
+ <file preprocess="xml-stripblanks">cc-input-list-box.ui</file>
+ </gresource>
+</gresources>
diff --git a/panels/keyboard/meson.build b/panels/keyboard/meson.build
new file mode 100644
index 0000000..cb88614
--- /dev/null
+++ b/panels/keyboard/meson.build
@@ -0,0 +1,106 @@
+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(
+ 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-system.xml',
+ '50-accessibility.xml'
+]
+
+foreach file: xml_files
+ i18n.merge_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-xkb-modifier-dialog.c',
+ 'cc-keyboard-shortcut-row.c',
+ 'cc-keyboard-shortcut-dialog.c',
+ 'cc-keyboard-panel.c',
+ 'cc-keyboard-item.c',
+ 'cc-keyboard-manager.c',
+ 'cc-keyboard-shortcut-editor.c',
+ 'keyboard-shortcuts.c',
+ 'cc-ibus-utils.c',
+ 'cc-input-chooser.c',
+ 'cc-input-row.c',
+ 'cc-input-source.c',
+ 'cc-input-source-ibus.c',
+ 'cc-input-list-box.c',
+ 'cc-input-source-xkb.c',
+)
+
+resource_data = files(
+ 'enter-keyboard-shortcut.svg',
+ 'cc-keyboard-panel.ui',
+ 'cc-keyboard-shortcut-editor.ui',
+ 'cc-input-chooser.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
+]
+
+if enable_ibus
+ deps += ibus_dep
+endif
+
+keyboard_panel_lib = static_library(
+ cappletname,
+ sources: sources,
+ include_directories: [top_inc, common_inc],
+ dependencies: deps,
+ c_args: cflags
+)
+panels_libs += keyboard_panel_lib
+
+subdir('icons')