From ae1c76ff830d146d41e88d6fba724c0a54bce868 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:45:20 +0200 Subject: Adding upstream version 1:43.6. Signed-off-by: Daniel Baumann --- panels/keyboard/00-multimedia.xml.in | 27 + panels/keyboard/01-input-sources.xml.in | 15 + panels/keyboard/01-launchers.xml.in | 19 + panels/keyboard/01-system.xml.in | 9 + panels/keyboard/50-accessibility.xml.in | 20 + panels/keyboard/cc-ibus-utils.c | 43 + panels/keyboard/cc-ibus-utils.h | 26 + panels/keyboard/cc-input-chooser.c | 1087 ++++++++++++++++++++ panels/keyboard/cc-input-chooser.h | 41 + panels/keyboard/cc-input-chooser.ui | 102 ++ panels/keyboard/cc-input-list-box.c | 821 +++++++++++++++ panels/keyboard/cc-input-list-box.h | 43 + panels/keyboard/cc-input-list-box.ui | 47 + panels/keyboard/cc-input-row.c | 306 ++++++ panels/keyboard/cc-input-row.h | 41 + panels/keyboard/cc-input-row.ui | 60 ++ panels/keyboard/cc-input-source-ibus.c | 155 +++ panels/keyboard/cc-input-source-ibus.h | 46 + panels/keyboard/cc-input-source-xkb.c | 134 +++ panels/keyboard/cc-input-source-xkb.h | 39 + panels/keyboard/cc-input-source.c | 84 ++ panels/keyboard/cc-input-source.h | 49 + panels/keyboard/cc-keyboard-item.c | 881 ++++++++++++++++ panels/keyboard/cc-keyboard-item.h | 112 ++ panels/keyboard/cc-keyboard-manager.c | 1050 +++++++++++++++++++ panels/keyboard/cc-keyboard-manager.h | 54 + panels/keyboard/cc-keyboard-panel.c | 266 +++++ panels/keyboard/cc-keyboard-panel.h | 32 + panels/keyboard/cc-keyboard-panel.ui | 116 +++ panels/keyboard/cc-keyboard-shortcut-dialog.c | 877 ++++++++++++++++ panels/keyboard/cc-keyboard-shortcut-dialog.h | 35 + panels/keyboard/cc-keyboard-shortcut-dialog.ui | 227 ++++ panels/keyboard/cc-keyboard-shortcut-editor.c | 985 ++++++++++++++++++ panels/keyboard/cc-keyboard-shortcut-editor.h | 52 + panels/keyboard/cc-keyboard-shortcut-editor.ui | 288 ++++++ panels/keyboard/cc-keyboard-shortcut-row.c | 139 +++ panels/keyboard/cc-keyboard-shortcut-row.h | 38 + panels/keyboard/cc-keyboard-shortcut-row.ui | 38 + panels/keyboard/cc-xkb-modifier-dialog.c | 355 +++++++ panels/keyboard/cc-xkb-modifier-dialog.h | 49 + panels/keyboard/cc-xkb-modifier-dialog.ui | 73 ++ panels/keyboard/enter-keyboard-shortcut.svg | 245 +++++ panels/keyboard/gnome-keybindings.pc.in | 10 + panels/keyboard/gnome-keyboard-panel.desktop.in.in | 18 + panels/keyboard/icons/meson.build | 4 + .../org.gnome.Settings-keyboard-symbolic.svg | 4 + panels/keyboard/keyboard-shortcuts.c | 450 ++++++++ panels/keyboard/keyboard-shortcuts.h | 84 ++ panels/keyboard/keyboard.gresource.xml | 14 + panels/keyboard/meson.build | 106 ++ 50 files changed, 9816 insertions(+) create mode 100644 panels/keyboard/00-multimedia.xml.in create mode 100644 panels/keyboard/01-input-sources.xml.in create mode 100644 panels/keyboard/01-launchers.xml.in create mode 100644 panels/keyboard/01-system.xml.in create mode 100644 panels/keyboard/50-accessibility.xml.in create mode 100644 panels/keyboard/cc-ibus-utils.c create mode 100644 panels/keyboard/cc-ibus-utils.h create mode 100644 panels/keyboard/cc-input-chooser.c create mode 100644 panels/keyboard/cc-input-chooser.h create mode 100644 panels/keyboard/cc-input-chooser.ui create mode 100644 panels/keyboard/cc-input-list-box.c create mode 100644 panels/keyboard/cc-input-list-box.h create mode 100644 panels/keyboard/cc-input-list-box.ui create mode 100644 panels/keyboard/cc-input-row.c create mode 100644 panels/keyboard/cc-input-row.h create mode 100644 panels/keyboard/cc-input-row.ui create mode 100644 panels/keyboard/cc-input-source-ibus.c create mode 100644 panels/keyboard/cc-input-source-ibus.h create mode 100644 panels/keyboard/cc-input-source-xkb.c create mode 100644 panels/keyboard/cc-input-source-xkb.h create mode 100644 panels/keyboard/cc-input-source.c create mode 100644 panels/keyboard/cc-input-source.h create mode 100644 panels/keyboard/cc-keyboard-item.c create mode 100644 panels/keyboard/cc-keyboard-item.h create mode 100644 panels/keyboard/cc-keyboard-manager.c create mode 100644 panels/keyboard/cc-keyboard-manager.h create mode 100644 panels/keyboard/cc-keyboard-panel.c create mode 100644 panels/keyboard/cc-keyboard-panel.h create mode 100644 panels/keyboard/cc-keyboard-panel.ui create mode 100644 panels/keyboard/cc-keyboard-shortcut-dialog.c create mode 100644 panels/keyboard/cc-keyboard-shortcut-dialog.h create mode 100644 panels/keyboard/cc-keyboard-shortcut-dialog.ui create mode 100644 panels/keyboard/cc-keyboard-shortcut-editor.c create mode 100644 panels/keyboard/cc-keyboard-shortcut-editor.h create mode 100644 panels/keyboard/cc-keyboard-shortcut-editor.ui create mode 100644 panels/keyboard/cc-keyboard-shortcut-row.c create mode 100644 panels/keyboard/cc-keyboard-shortcut-row.h create mode 100644 panels/keyboard/cc-keyboard-shortcut-row.ui create mode 100644 panels/keyboard/cc-xkb-modifier-dialog.c create mode 100644 panels/keyboard/cc-xkb-modifier-dialog.h create mode 100644 panels/keyboard/cc-xkb-modifier-dialog.ui create mode 100644 panels/keyboard/enter-keyboard-shortcut.svg create mode 100644 panels/keyboard/gnome-keybindings.pc.in create mode 100644 panels/keyboard/gnome-keyboard-panel.desktop.in.in create mode 100644 panels/keyboard/icons/meson.build create mode 100644 panels/keyboard/icons/scalable/org.gnome.Settings-keyboard-symbolic.svg create mode 100644 panels/keyboard/keyboard-shortcuts.c create mode 100644 panels/keyboard/keyboard-shortcuts.h create mode 100644 panels/keyboard/keyboard.gresource.xml create mode 100644 panels/keyboard/meson.build (limited to 'panels/keyboard') 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + Search + + + 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 @@ + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + 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 . + */ + +#include + +#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 . + */ + +#pragma once + +#include + +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 . + */ + +#include +#include +#include + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#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 +#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 . + */ + +#pragma once + +#include + +#include "cc-input-source.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +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 @@ + + + + + + + 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 . + * + * Author: Sergey Udaltsov + * Ian Douglas Scott + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#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 +#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 . + * + * Author: Sergey Udaltsov + * Ian Douglas Scott + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +G_BEGIN_DECLS + +#include + +#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 @@ + + + + + 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 . + */ + +#include +#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 . + */ + +#pragma once + +#include +#include +#include + +#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 @@ + + + + + +
+ + Move Up + row.move-up + + + Move Down + row.move-down + +
+
+ + Preferences + row.show-settings + action-disabled + +
+
+ + View Keyboard Layout + row.show-layout + +
+
+ + Remove + row.remove + action-disabled + +
+
+
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 . + */ + +#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 . + */ + +#pragma once + +#include + +#ifdef HAVE_IBUS +#include +#endif + +#include + +#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 . + */ + +#include +#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 . + */ + +#pragma once + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#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 . + */ + +#include +#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 . + */ + +#pragma once + +#include + +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 . + * + */ + +#include "config.h" + +#include +#include + +#include +#include +#include + +#include "cc-keyboard-item.h" + +#define CUSTOM_KEYS_SCHEMA "org.gnome.settings-daemon.plugins.media-keys.custom-keybinding" + +struct _CcKeyboardItem +{ + GObject parent_instance; + + CcKeyboardItem *reverse_item; + gboolean is_reversed; + gboolean hidden; + + CcKeyboardItemType type; + + BindingGroupType group; + char *description; + gboolean editable; + GList *key_combos; + GList *default_combos; + gboolean can_set_multiple; + + /* GSettings path */ + char *gsettings_path; + gboolean desc_editable; + char *command; + gboolean cmd_editable; + + /* GSettings */ + char *schema; + char *key; + GSettings *settings; +}; + +enum +{ + PROP_0, + PROP_DESCRIPTION, + PROP_EDITABLE, + PROP_TYPE, + PROP_IS_VALUE_DEFAULT, + PROP_COMMAND, + PROP_KEY_COMBOS +}; + +static void cc_keyboard_item_class_init (CcKeyboardItemClass *klass); +static void cc_keyboard_item_init (CcKeyboardItem *keyboard_item); +static void cc_keyboard_item_finalize (GObject *object); + +G_DEFINE_TYPE (CcKeyboardItem, cc_keyboard_item, G_TYPE_OBJECT) + +static const CcKeyCombo EMPTY_COMBO = { 0, 0, 0 }; + +static gboolean +combo_equal (CcKeyCombo *a, CcKeyCombo *b) +{ + return (a->keyval == b->keyval + && a->keycode == b->keycode + && a->mask == b->mask); +} + +static gboolean +combos_contains (GList *combos, CcKeyCombo *needle) +{ + for (GList *l = combos; l != NULL; l = l->next) + { + if (combo_equal (l->data, needle)) + return TRUE; + } + + return FALSE; +} + +static gboolean +combos_equal (GList *a, GList *b) +{ + // Should be efficient enough for any sane number of bindings + + for (GList *l = a; l != NULL; l = l->next) + { + if (!combos_contains (b, l->data)) + return FALSE; + } + + for (GList *l = b; l != NULL; l = l->next) + { + if (!combos_contains (a, l->data)) + return FALSE; + } + + return TRUE; +} + +static gboolean +binding_from_string (const char *str, + CcKeyCombo *combo) +{ + g_return_val_if_fail (combo != NULL, FALSE); + g_autofree guint *keycodes = NULL; + + if (str == NULL || strcmp (str, "disabled") == 0) + { + memset (combo, 0, sizeof(CcKeyCombo)); + return TRUE; + } + + gtk_accelerator_parse_with_keycode (str, + gdk_display_get_default (), + &combo->keyval, + &keycodes, + &combo->mask); + + combo->keycode = (keycodes ? keycodes[0] : 0); + + if (combo->keyval == 0) + return FALSE; + else + return TRUE; +} + +static void +_set_description (CcKeyboardItem *item, + const char *value) +{ + g_free (item->description); + item->description = g_strdup (value); +} + +const char * +cc_keyboard_item_get_description (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + + return item->description; +} + +gboolean +cc_keyboard_item_get_desc_editable (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE); + + return item->desc_editable; +} + +static void +_set_type (CcKeyboardItem *item, + gint value) +{ + item->type = value; +} + +static void +_set_command (CcKeyboardItem *item, + const char *value) +{ + g_free (item->command); + item->command = g_strdup (value); +} + +const char * +cc_keyboard_item_get_command (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + + return item->command; +} + +gboolean +cc_keyboard_item_get_cmd_editable (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), FALSE); + + return item->cmd_editable; +} + +static void +cc_keyboard_item_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcKeyboardItem *self; + + self = CC_KEYBOARD_ITEM (object); + + switch (prop_id) { + case PROP_DESCRIPTION: + _set_description (self, g_value_get_string (value)); + break; + case PROP_COMMAND: + _set_command (self, g_value_get_string (value)); + break; + case PROP_TYPE: + _set_type (self, g_value_get_int (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cc_keyboard_item_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcKeyboardItem *self; + + self = CC_KEYBOARD_ITEM (object); + + switch (prop_id) { + case PROP_DESCRIPTION: + g_value_set_string (value, self->description); + break; + case PROP_EDITABLE: + g_value_set_boolean (value, self->editable); + break; + case PROP_COMMAND: + g_value_set_string (value, self->command); + break; + case PROP_IS_VALUE_DEFAULT: + g_value_set_boolean (value, cc_keyboard_item_is_value_default (self)); + break; + case PROP_KEY_COMBOS: + g_value_set_pointer (value, self->key_combos); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +cc_keyboard_item_class_init (CcKeyboardItemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = cc_keyboard_item_get_property; + object_class->set_property = cc_keyboard_item_set_property; + object_class->finalize = cc_keyboard_item_finalize; + + g_object_class_install_property (object_class, + PROP_DESCRIPTION, + g_param_spec_string ("description", + "description", + "description", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_EDITABLE, + g_param_spec_boolean ("editable", + NULL, + NULL, + FALSE, + G_PARAM_READABLE)); + + g_object_class_install_property (object_class, + PROP_TYPE, + g_param_spec_int ("type", + NULL, + NULL, + CC_KEYBOARD_ITEM_TYPE_NONE, + CC_KEYBOARD_ITEM_TYPE_GSETTINGS, + CC_KEYBOARD_ITEM_TYPE_NONE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); + + g_object_class_install_property (object_class, + PROP_COMMAND, + g_param_spec_string ("command", + "command", + "command", + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property (object_class, + PROP_IS_VALUE_DEFAULT, + g_param_spec_boolean ("is-value-default", + "is value default", + "is value default", + TRUE, + G_PARAM_READABLE)); + + g_object_class_install_property (object_class, + PROP_KEY_COMBOS, + g_param_spec_pointer ("key-combos", + "key combos", + "key combos", + G_PARAM_READABLE)); +} + +static void +cc_keyboard_item_init (CcKeyboardItem *item) +{ +} + +static void +cc_keyboard_item_finalize (GObject *object) +{ + CcKeyboardItem *item; + + g_return_if_fail (object != NULL); + g_return_if_fail (CC_IS_KEYBOARD_ITEM (object)); + + item = CC_KEYBOARD_ITEM (object); + + if (item->settings != NULL) + g_object_unref (item->settings); + + /* Free memory */ + g_free (item->gsettings_path); + g_free (item->description); + g_free (item->command); + g_free (item->schema); + g_free (item->key); + g_list_free_full (item->key_combos, g_free); + g_list_free_full (item->default_combos, g_free); + + G_OBJECT_CLASS (cc_keyboard_item_parent_class)->finalize (object); +} + +CcKeyboardItem * +cc_keyboard_item_new (CcKeyboardItemType type) +{ + GObject *object; + + object = g_object_new (CC_TYPE_KEYBOARD_ITEM, + "type", type, + NULL); + + return CC_KEYBOARD_ITEM (object); +} + +static guint * +get_above_tab_keysyms (void) +{ + guint keycode = 0x29 /* KEY_GRAVE */ + 8; + g_autofree guint *keyvals = NULL; + GArray *keysyms; + int n_entries, i, j; + + keysyms = g_array_new (TRUE, FALSE, sizeof (guint)); + + if (!gdk_display_map_keycode (gdk_display_get_default (), + keycode, + NULL, + &keyvals, + &n_entries)) + goto out; + + for (i = 0; i < n_entries; i++) + { + gboolean found = FALSE; + + for (j = 0; j < keysyms->len; j++) + if (g_array_index (keysyms, guint, j) == keyvals[i]) + { + found = TRUE; + break; + } + + if (!found) + g_array_append_val (keysyms, keyvals[i]); + } + +out: + return (guint *)g_array_free (keysyms, FALSE); +} + +/* + * translate_above_tab: + * + * @original_bindings: A list of accelerator strings + * @new_bindings: (out): Translated bindings if translation is needed + * + * Translate accelerator strings that contain the Above_Tab fake keysym + * used by mutter to strings that use the real keysyms that correspond + * to the key that is located physically above the tab key. + * + * Returns: %TRUE if strings were translated, %FALSE if @original_bindings + * can be used unmodified + */ +static gboolean +translate_above_tab (char **original_bindings, + char ***new_bindings) +{ + GPtrArray *replaced_bindings; + g_autofree guint *above_tab_keysyms = NULL; + gboolean needs_translation = FALSE; + char **str; + + for (str = original_bindings; *str && !needs_translation; str++) + needs_translation = strstr (*str, "Above_Tab") != NULL; + + if (!needs_translation) + return FALSE; + + above_tab_keysyms = get_above_tab_keysyms (); + + replaced_bindings = g_ptr_array_new (); + + for (str = original_bindings; *str; str++) + { + if (strstr (*str, "Above_Tab") == NULL) + { + g_ptr_array_add (replaced_bindings, g_strdup (*str)); + } + else + { + g_auto (GStrv) split_str = g_strsplit (*str, "Above_Tab", -1); + int i; + + for (i = 0; above_tab_keysyms[i]; i++) + { + g_autofree char *sym = NULL; + + sym = gtk_accelerator_name (above_tab_keysyms[i], 0); + g_ptr_array_add (replaced_bindings, g_strjoinv (sym, split_str)); + } + } + g_ptr_array_add (replaced_bindings, NULL); + } + + *new_bindings = (char **)g_ptr_array_free (replaced_bindings, FALSE); + return TRUE; +} + +static GList * +variant_get_key_combos (GVariant *variant) +{ + GList *combos = NULL; + char **translated_bindings, **str; + g_auto(GStrv) bindings = NULL; + + if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING)) + { + bindings = g_malloc0_n (2, sizeof(char *)); + if (g_strcmp0 (g_variant_get_string (variant, NULL), "") != 0) + bindings[0] = g_variant_dup_string (variant, NULL); + } + else if (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY)) + { + bindings = g_variant_dup_strv (variant, NULL); + } + + if (translate_above_tab (bindings, &translated_bindings)) + { + g_strfreev (bindings); + bindings = translated_bindings; + } + + for (str = bindings; *str; str++) + { + g_autofree CcKeyCombo *combo = g_new (CcKeyCombo, 1); + + binding_from_string (*str, combo); + + if (combo->keyval != 0 || combo->keycode != 0 || combo->mask != 0) + combos = g_list_prepend (combos, g_steal_pointer (&combo)); + } + + return g_list_reverse (combos); +} + +static GList * +settings_get_key_combos (GSettings *settings, + const char *key, + gboolean use_default) +{ + GList *key_combos; + g_autoptr(GVariant) variant = NULL; + + if (use_default) + variant = g_settings_get_default_value (settings, key); + else + variant = g_settings_get_value (settings, key); + key_combos = variant_get_key_combos (variant); + + return key_combos; +} + +static void +binding_changed (CcKeyboardItem *item, + const char *key) +{ + g_list_free_full (item->key_combos, g_free); + item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE); + + item->editable = g_settings_is_writable (item->settings, item->key); + + g_object_notify (G_OBJECT (item), "key-combos"); +} + +gboolean +cc_keyboard_item_load_from_gsettings_path (CcKeyboardItem *item, + const char *path, + gboolean reset) +{ + g_autoptr(GVariant) variant = NULL; + + item->schema = g_strdup (CUSTOM_KEYS_SCHEMA); + item->gsettings_path = g_strdup (path); + item->key = g_strdup ("binding"); + item->settings = g_settings_new_with_path (item->schema, path); + item->editable = g_settings_is_writable (item->settings, item->key); + item->desc_editable = g_settings_is_writable (item->settings, "name"); + item->cmd_editable = g_settings_is_writable (item->settings, "command"); + + variant = g_settings_get_value (item->settings, item->key); + item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY); + + if (reset) + { + g_settings_reset (item->settings, "name"); + g_settings_reset (item->settings, "command"); + g_settings_reset (item->settings, "binding"); + } + + g_settings_bind (item->settings, "name", + G_OBJECT (item), "description", G_SETTINGS_BIND_DEFAULT); + g_settings_bind (item->settings, "command", + G_OBJECT (item), "command", G_SETTINGS_BIND_DEFAULT); + + g_list_free_full (item->key_combos, g_free); + item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE); + + g_signal_connect_object (G_OBJECT (item->settings), "changed::binding", + G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED); + + return TRUE; +} + +gboolean +cc_keyboard_item_load_from_gsettings (CcKeyboardItem *item, + const char *description, + const char *schema, + const char *key) +{ + g_autofree char *signal_name = NULL; + g_autoptr(GVariant) variant = NULL; + + item->schema = g_strdup (schema); + item->key = g_strdup (key); + item->description = g_strdup (description); + + item->settings = g_settings_new (item->schema); + item->editable = g_settings_is_writable (item->settings, item->key); + + g_list_free_full (item->key_combos, g_free); + item->key_combos = settings_get_key_combos (item->settings, item->key, FALSE); + + g_list_free_full (item->default_combos, g_free); + item->default_combos = settings_get_key_combos (item->settings, item->key, TRUE); + + variant = g_settings_get_value (item->settings, item->key); + item->can_set_multiple = g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING_ARRAY); + + signal_name = g_strdup_printf ("changed::%s", item->key); + g_signal_connect_object (G_OBJECT (item->settings), signal_name, + G_CALLBACK (binding_changed), item, G_CONNECT_SWAPPED); + + return TRUE; +} + +gboolean +cc_keyboard_item_equal (CcKeyboardItem *a, + CcKeyboardItem *b) +{ + if (a->type != b->type) + return FALSE; + switch (a->type) + { + case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH: + return g_str_equal (a->gsettings_path, b->gsettings_path); + case CC_KEYBOARD_ITEM_TYPE_GSETTINGS: + return (g_str_equal (a->schema, b->schema) && + g_str_equal (a->key, b->key)); + default: + g_assert_not_reached (); + } + +} + +void +cc_keyboard_item_add_reverse_item (CcKeyboardItem *item, + CcKeyboardItem *reverse_item, + gboolean is_reversed) +{ + g_return_if_fail (item->key != NULL); + + item->reverse_item = reverse_item; + if (reverse_item->reverse_item == NULL) + { + reverse_item->reverse_item = item; + reverse_item->is_reversed = !is_reversed; + } + else + g_warn_if_fail (reverse_item->is_reversed == !!is_reversed); + + item->is_reversed = !!is_reversed; +} + +CcKeyboardItem * +cc_keyboard_item_get_reverse_item (CcKeyboardItem *item) +{ + return item->reverse_item; +} + + +void +cc_keyboard_item_set_hidden (CcKeyboardItem *item, gboolean hidden) +{ + item->hidden = !!hidden; +} + + +gboolean +cc_keyboard_item_is_hidden (CcKeyboardItem *item) +{ + return item->hidden; +} + +/** + * cc_keyboard_item_is_value_default: + * @self: a #CcKeyboardItem + * + * Retrieves whether the shortcut is the default value or not. + * + * Returns: %TRUE if the shortcut is the default value, %FALSE otherwise. + */ +gboolean +cc_keyboard_item_is_value_default (CcKeyboardItem *self) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (self), FALSE); + + /* + * When the shortcut is custom, we don't treat it as modified + * since we don't know what would be its default value. + */ + if (self->type == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH) + return TRUE; + + return combos_equal (self->default_combos, self->key_combos); +} + +/** + * cc_keyboard_item_reset: + * @self: a #CcKeyboardItem + * + * Reset the keyboard binding to the default value. + */ +void +cc_keyboard_item_reset (CcKeyboardItem *self) +{ + CcKeyboardItem *reverse; + + g_return_if_fail (CC_IS_KEYBOARD_ITEM (self)); + + reverse = self->reverse_item; + + g_settings_reset (self->settings, self->key); + g_object_notify (G_OBJECT (self), "is-value-default"); + + /* Also reset the reverse item */ + if (reverse) + { + g_settings_reset (reverse->settings, reverse->key); + g_object_notify (G_OBJECT (reverse), "is-value-default"); + } +} + +GList * +cc_keyboard_item_get_key_combos (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + return item->key_combos; +} + +GList * +cc_keyboard_item_get_default_combos (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + return item->default_combos; +} + +CcKeyCombo +cc_keyboard_item_get_primary_combo (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), EMPTY_COMBO); + return (item->key_combos) ? *((CcKeyCombo*)item->key_combos->data) : EMPTY_COMBO; +} + +const gchar * +cc_keyboard_item_get_key (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + return item->key; +} + +CcKeyboardItemType +cc_keyboard_item_get_item_type (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), CC_KEYBOARD_ITEM_TYPE_NONE); + return item->type; +} + +const gchar * +cc_keyboard_item_get_gsettings_path (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + return item->gsettings_path; +} + +GSettings * +cc_keyboard_item_get_settings (CcKeyboardItem *item) +{ + g_return_val_if_fail (CC_IS_KEYBOARD_ITEM (item), NULL); + return item->settings; +} + +gboolean +cc_keyboard_item_can_set_multiple (CcKeyboardItem *item) +{ + return item->can_set_multiple; +} + +static gchar* +combo_get_accelerator (CcKeyCombo *combo) +{ + return gtk_accelerator_name_with_keycode (NULL, + combo->keyval, + combo->keycode, + combo->mask); +} + +static void +cc_keyboard_item_add_key_combo_inner (CcKeyboardItem *self, + CcKeyCombo *combo) +{ + g_auto(GStrv) strv = NULL; + int i; + + if (!self->can_set_multiple) + { + g_settings_set_string (self->settings, self->key, combo_get_accelerator (combo)); + } + else + { + strv = g_new0 (gchar*, g_list_length (self->key_combos) + 2); + + i = 0; + for (GList *l = self->key_combos; l != NULL; l = l->next, i++) + { + if (combo_equal (l->data, combo)) + // This combo is already in the list + return; + strv[i] = combo_get_accelerator (l->data); + } + strv[i] = combo_get_accelerator (combo); + + g_settings_set_strv (self->settings, self->key, (const gchar **)strv); + } + + binding_changed (self, self->key); +} + +void +cc_keyboard_item_add_key_combo (CcKeyboardItem *self, + CcKeyCombo *combo) +{ + CcKeyCombo reverse_combo; + + if (self->reverse_item) + { + reverse_combo.keyval = combo->keyval; + reverse_combo.keycode = combo->keycode; + reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK; + cc_keyboard_item_add_key_combo_inner (self->reverse_item, &reverse_combo); + } + + cc_keyboard_item_add_key_combo_inner (self, combo); +} + +static void +cc_keyboard_item_remove_key_combo_inner (CcKeyboardItem *self, + CcKeyCombo *combo) +{ + g_auto(GStrv) strv = NULL; + gboolean found; + int i; + + strv = g_new0 (gchar*, g_list_length (self->key_combos) + 1); + + found = FALSE; + i = 0; + for (GList *l = self->key_combos; l != NULL; l = l->next, i++) + { + if (combo_equal (l->data, combo)) + { + i--; + found = TRUE; + } + else + { + strv[i] = combo_get_accelerator (l->data); + } + } + + if (found) + { + if (self->can_set_multiple) + g_settings_set_strv (self->settings, self->key, (const gchar **)strv); + else + g_settings_set_string (self->settings, self->key, ""); + } + + binding_changed (self, self->key); +} + +void +cc_keyboard_item_remove_key_combo (CcKeyboardItem *self, + CcKeyCombo *combo) +{ + CcKeyCombo reverse_combo; + + if (self->reverse_item) + { + reverse_combo.keyval = combo->keyval; + reverse_combo.keycode = combo->keycode; + reverse_combo.mask = combo->mask ^ GDK_SHIFT_MASK; + cc_keyboard_item_remove_key_combo_inner (self->reverse_item, &reverse_combo); + } + + cc_keyboard_item_remove_key_combo_inner (self, combo); +} + +void cc_keyboard_item_disable (CcKeyboardItem *self) +{ + if (!self->can_set_multiple) + { + g_settings_set_string (self->settings, self->key, ""); + if (self->reverse_item) + g_settings_set_string (self->reverse_item->settings, self->reverse_item->key, ""); + } + else + { + g_settings_set_strv (self->settings, self->key, NULL); + if (self->reverse_item) + g_settings_set_strv (self->reverse_item->settings, self->reverse_item->key, NULL); + } + + binding_changed (self, self->key); +} 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 . + * + */ + +#pragma once + +#include +#include + +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 . + * + * Author: Thomas Wood + * Georges Basile Stavracas Neto + * + */ + +#include + +#include "cc-keyboard-manager.h" +#include "keyboard-shortcuts.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#include +#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, §ions_iter); + + while (can_continue) + { + BindingGroupType group; + GPtrArray *keys; + g_autofree gchar *id = NULL; + g_autofree gchar *title = NULL; + gint i; + + gtk_tree_model_get (sections_model, + §ions_iter, + SECTION_DESCRIPTION_COLUMN, &title, + SECTION_GROUP_COLUMN, &group, + SECTION_ID_COLUMN, &id, + -1); + + /* Ignore separators */ + if (group == BINDING_GROUP_SEPARATOR) + { + can_continue = gtk_tree_model_iter_next (sections_model, §ions_iter); + continue; + } + + keys = g_hash_table_lookup (get_hash_for_group (self, group), id); + + for (i = 0; i < keys->len; i++) + { + CcKeyboardItem *item = g_ptr_array_index (keys, i); + + if (!cc_keyboard_item_is_hidden (item)) + { + g_signal_emit (self, signals[SHORTCUT_ADDED], + 0, + item, + id, + title); + } + } + + can_continue = gtk_tree_model_iter_next (sections_model, §ions_iter); + } +} + +static void +append_section (CcKeyboardManager *self, + const gchar *title, + const gchar *id, + BindingGroupType group, + const KeyListEntry *keys_list) +{ + GtkTreeIter iter; + GHashTable *reverse_items; + GHashTable *hash; + GPtrArray *keys_array; + gboolean is_new; + gint i; + + hash = get_hash_for_group (self, group); + + if (!hash) + return; + + /* Add all CcKeyboardItems for this section */ + is_new = FALSE; + keys_array = g_hash_table_lookup (hash, id); + if (keys_array == NULL) + { + keys_array = g_ptr_array_new (); + is_new = TRUE; + } + + reverse_items = g_hash_table_new (g_str_hash, g_str_equal); + + for (i = 0; keys_list != NULL && keys_list[i].name != NULL; i++) + { + CcKeyboardItem *item; + gboolean ret; + + if (have_key_for_group (self, group, keys_list[i].name)) + continue; + + item = cc_keyboard_item_new (keys_list[i].type); + + switch (keys_list[i].type) + { + case CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH: + ret = cc_keyboard_item_load_from_gsettings_path (item, keys_list[i].name, FALSE); + break; + + case CC_KEYBOARD_ITEM_TYPE_GSETTINGS: + ret = cc_keyboard_item_load_from_gsettings (item, + keys_list[i].description, + keys_list[i].schema, + keys_list[i].name); + if (ret && keys_list[i].reverse_entry != NULL) + { + CcKeyboardItem *reverse_item; + reverse_item = g_hash_table_lookup (reverse_items, + keys_list[i].reverse_entry); + if (reverse_item != NULL) + { + cc_keyboard_item_add_reverse_item (item, + reverse_item, + keys_list[i].is_reversed); + } + else + { + g_hash_table_insert (reverse_items, + keys_list[i].name, + item); + } + } + break; + + default: + g_assert_not_reached (); + } + + if (ret == FALSE) + { + /* We don't actually want to popup a dialog - just skip this one */ + g_object_unref (item); + continue; + } + + cc_keyboard_item_set_hidden (item, keys_list[i].hidden); + + g_ptr_array_add (keys_array, item); + } + + g_hash_table_destroy (reverse_items); + + /* Add the keys to the hash table */ + if (is_new) + { + g_hash_table_insert (hash, g_strdup (id), keys_array); + + /* Append the section to the left tree view */ + gtk_list_store_append (GTK_LIST_STORE (self->sections_store), &iter); + gtk_list_store_set (GTK_LIST_STORE (self->sections_store), + &iter, + SECTION_DESCRIPTION_COLUMN, title, + SECTION_ID_COLUMN, id, + SECTION_GROUP_COLUMN, group, + -1); + } +} + +static void +append_sections_from_file (CcKeyboardManager *self, + const gchar *path, + const char *datadir, + gchar **wm_keybindings) +{ + KeyList *keylist; + KeyListEntry *keys; + KeyListEntry key = { 0, 0, 0, 0, 0, 0, 0 }; + const char *title; + int group; + guint i; + + keylist = parse_keylist_from_file (path); + + if (keylist == NULL) + return; + +#define const_strv(s) ((const gchar* const*) s) + + /* If there's no keys to add, or the settings apply to a window manager + * that's not the one we're running */ + if (keylist->entries->len == 0 || + (keylist->wm_name != NULL && !g_strv_contains (const_strv (wm_keybindings), keylist->wm_name)) || + keylist->name == NULL) + { + g_free (keylist->name); + g_free (keylist->package); + g_free (keylist->wm_name); + g_array_free (keylist->entries, TRUE); + g_free (keylist); + return; + } + +#undef const_strv + + /* Empty KeyListEntry to end the array */ + key.name = NULL; + g_array_append_val (keylist->entries, key); + + keys = (KeyListEntry *) g_array_free (keylist->entries, FALSE); + if (keylist->package) + { + g_autofree gchar *localedir = NULL; + + localedir = g_build_filename (datadir, "locale", NULL); + bindtextdomain (keylist->package, localedir); + + title = dgettext (keylist->package, keylist->name); + } else { + title = _(keylist->name); + } + + if (keylist->group && strcmp (keylist->group, "system") == 0) + group = BINDING_GROUP_SYSTEM; + else + group = BINDING_GROUP_APPS; + + append_section (self, title, keylist->name, group, keys); + + g_free (keylist->name); + g_free (keylist->package); + g_free (keylist->wm_name); + g_free (keylist->schema); + g_free (keylist->group); + + for (i = 0; keys[i].name != NULL; i++) + { + KeyListEntry *entry = &keys[i]; + g_free (entry->schema); + g_free (entry->description); + g_free (entry->name); + g_free (entry->reverse_entry); + } + + g_free (keylist); + g_free (keys); +} + +static void +append_sections_from_gsettings (CcKeyboardManager *self) +{ + g_auto(GStrv) custom_paths = NULL; + GArray *entries; + KeyListEntry key = { 0, 0, 0, 0, 0, 0, 0 }; + int i; + + /* load custom shortcuts from GSettings */ + entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry)); + + custom_paths = g_settings_get_strv (self->binding_settings, "custom-keybindings"); + for (i = 0; custom_paths[i]; i++) + { + key.name = g_strdup (custom_paths[i]); + if (!have_key_for_group (self, BINDING_GROUP_USER, key.name)) + { + key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH; + g_array_append_val (entries, key); + } + else + g_free (key.name); + } + + if (entries->len > 0) + { + KeyListEntry *keys; + int i; + + /* Empty KeyListEntry to end the array */ + key.name = NULL; + g_array_append_val (entries, key); + + keys = (KeyListEntry *) entries->data; + append_section (self, _("Custom Shortcuts"), CUSTOM_SHORTCUTS_ID, BINDING_GROUP_USER, keys); + for (i = 0; i < entries->len; ++i) + { + g_free (keys[i].name); + } + } + else + { + append_section (self, _("Custom Shortcuts"), CUSTOM_SHORTCUTS_ID, BINDING_GROUP_USER, NULL); + } + + g_array_free (entries, TRUE); +} + +#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 . + * + * Author: Thomas Wood + * Georges Basile Stavracas Neto + * + */ + +#pragma once + +#include + +#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 . + * + * Author: Thomas Wood + * Georges Basile Stavracas Neto + * Ian Douglas Scott + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include + +#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 C and o will enter ©, " + "a followed by ' will enter á."), + (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 . + * + * Author: Thomas Wood + * + */ + + +#pragma once + +#include +#include + +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 @@ + + + + 100 + 2500 + 1000 + 200 + 200 + + + 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 . + * + * Author: Thomas Wood + * Georges Basile Stavracas Neto + * Ian Douglas Scott + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include +#include + +#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 ("%s", _(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 . + * + * Author: Ian Douglas Scott + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +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 @@ + + + + + + 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 . + * + * Authors: Georges Basile Stavracas Neto + */ + +#include +#include + +#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 ("%s", 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 ("%s", 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 . + * + * Authors: Georges Basile Stavracas Neto + */ + +#pragma once + +#include + +#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 @@ + + + + + + + + + + + + + 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 . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#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 ("%s", 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 . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#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 @@ + + + + + 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 + * + * 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 . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include +#include + +#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 ":" 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 + * + * 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 . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +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 @@ + + + + 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + 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 . + * + * Authors: Thomas Wood + * Rodrigo Moya + * Christophe Fergeau + */ + +#include + +#include + +#include "keyboard-shortcuts.h" + +#define CUSTOM_KEYS_BASENAME "/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings" + +static char * +replace_pictures_folder (const char *description) +{ + g_autoptr(GRegex) pictures_regex = NULL; + const char *path; + g_autofree gchar *dirname = NULL; + g_autofree gchar *ret = NULL; + + if (description == NULL) + return NULL; + + if (strstr (description, "$PICTURES") == NULL) + return g_strdup (description); + + pictures_regex = g_regex_new ("\\$PICTURES", 0, 0, NULL); + path = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES); + dirname = g_filename_display_basename (path); + ret = g_regex_replace (pictures_regex, description, -1, + 0, dirname, 0, NULL); + + if (ret == NULL) + return g_strdup (description); + + return g_steal_pointer (&ret); +} + +static void +parse_start_tag (GMarkupParseContext *ctx, + const gchar *element_name, + const gchar **attr_names, + const gchar **attr_values, + gpointer user_data, + GError **error) +{ + KeyList *keylist = (KeyList *) user_data; + KeyListEntry key; + const char *name, *schema, *description, *package, *context, *orig_description, *reverse_entry; + gboolean is_reversed, hidden; + + name = NULL; + schema = NULL; + package = NULL; + context = NULL; + + /* The top-level element, names the section in the tree */ + if (g_str_equal (element_name, "KeyListEntries")) + { + const char *wm_name = NULL; + const char *group = NULL; + + while (*attr_names && *attr_values) + { + if (g_str_equal (*attr_names, "name")) + { + if (**attr_values) + name = *attr_values; + } else if (g_str_equal (*attr_names, "group")) { + if (**attr_values) + group = *attr_values; + } else if (g_str_equal (*attr_names, "wm_name")) { + if (**attr_values) + wm_name = *attr_values; + } else if (g_str_equal (*attr_names, "schema")) { + if (**attr_values) + schema = *attr_values; + } else if (g_str_equal (*attr_names, "package")) { + if (**attr_values) + package = *attr_values; + } + ++attr_names; + ++attr_values; + } + + if (name) + { + if (keylist->name) + g_warning ("Duplicate section name"); + g_free (keylist->name); + keylist->name = g_strdup (name); + } + if (wm_name) + { + if (keylist->wm_name) + g_warning ("Duplicate window manager name"); + g_free (keylist->wm_name); + keylist->wm_name = g_strdup (wm_name); + } + if (package) + { + if (keylist->package) + g_warning ("Duplicate gettext package name"); + g_free (keylist->package); + keylist->package = g_strdup (package); + bind_textdomain_codeset (keylist->package, "UTF-8"); + } + if (group) + { + if (keylist->group) + g_warning ("Duplicate group"); + g_free (keylist->group); + keylist->group = g_strdup (group); + } + if (schema) + { + if (keylist->schema) + g_warning ("Duplicate schema"); + g_free (keylist->schema); + keylist->schema = g_strdup (schema); + } + return; + } + + if (!g_str_equal (element_name, "KeyListEntry") + || attr_names == NULL + || attr_values == NULL) + return; + + schema = NULL; + description = NULL; + context = NULL; + orig_description = NULL; + reverse_entry = NULL; + is_reversed = FALSE; + hidden = FALSE; + + while (*attr_names && *attr_values) + { + if (g_str_equal (*attr_names, "name")) + { + /* skip if empty */ + if (**attr_values) + name = *attr_values; + } else if (g_str_equal (*attr_names, "schema")) { + if (**attr_values) { + schema = *attr_values; + } + } else if (g_str_equal (*attr_names, "description")) { + if (**attr_values) + orig_description = *attr_values; + } else if (g_str_equal (*attr_names, "msgctxt")) { + if (**attr_values) + context = *attr_values; + } else if (g_str_equal (*attr_names, "reverse-entry")) { + if (**attr_values) + reverse_entry = *attr_values; + } else if (g_str_equal (*attr_names, "is-reversed")) { + if (g_str_equal (*attr_values, "true")) + is_reversed = TRUE; + } else if (g_str_equal (*attr_names, "hidden")) { + if (g_str_equal (*attr_values, "true")) + hidden = TRUE; + } + + ++attr_names; + ++attr_values; + } + + if (name == NULL) + return; + + if (schema == NULL && + keylist->schema == NULL) { + g_debug ("Ignored GConf keyboard shortcut '%s'", name); + return; + } + + if (context != NULL) + description = g_dpgettext2 (keylist->package, context, orig_description); + else + description = dgettext (keylist->package, orig_description); + + key.name = g_strdup (name); + key.type = CC_KEYBOARD_ITEM_TYPE_GSETTINGS; + key.description = replace_pictures_folder (description); + key.schema = schema ? g_strdup (schema) : g_strdup (keylist->schema); + key.reverse_entry = g_strdup (reverse_entry); + key.is_reversed = is_reversed; + key.hidden = hidden; + g_array_append_val (keylist->entries, key); +} + +static const guint forbidden_keyvals[] = { + /* Navigation keys */ + GDK_KEY_Home, + GDK_KEY_Left, + GDK_KEY_Up, + GDK_KEY_Right, + GDK_KEY_Down, + GDK_KEY_Page_Up, + GDK_KEY_Page_Down, + GDK_KEY_End, + GDK_KEY_Tab, + + /* Return */ + GDK_KEY_KP_Enter, + GDK_KEY_Return, + + GDK_KEY_Mode_switch +}; + +static gboolean +keyval_is_forbidden (guint keyval) +{ + guint i; + + for (i = 0; i < G_N_ELEMENTS(forbidden_keyvals); i++) { + if (keyval == forbidden_keyvals[i]) + return TRUE; + } + + return FALSE; +} + +gboolean +is_valid_binding (const CcKeyCombo *combo) +{ + if ((combo->mask == 0 || combo->mask == GDK_SHIFT_MASK) && combo->keycode != 0) + { + guint keyval = combo->keyval; + + if ((keyval >= GDK_KEY_a && keyval <= GDK_KEY_z) + || (keyval >= GDK_KEY_A && keyval <= GDK_KEY_Z) + || (keyval >= GDK_KEY_0 && keyval <= GDK_KEY_9) + || (keyval >= GDK_KEY_kana_fullstop && keyval <= GDK_KEY_semivoicedsound) + || (keyval >= GDK_KEY_Arabic_comma && keyval <= GDK_KEY_Arabic_sukun) + || (keyval >= GDK_KEY_Serbian_dje && keyval <= GDK_KEY_Cyrillic_HARDSIGN) + || (keyval >= GDK_KEY_Greek_ALPHAaccent && keyval <= GDK_KEY_Greek_omega) + || (keyval >= GDK_KEY_hebrew_doublelowline && keyval <= GDK_KEY_hebrew_taf) + || (keyval >= GDK_KEY_Thai_kokai && keyval <= GDK_KEY_Thai_lekkao) + || (keyval >= GDK_KEY_Hangul_Kiyeog && keyval <= GDK_KEY_Hangul_J_YeorinHieuh) + || (keyval == GDK_KEY_space && combo->mask == 0) + || keyval_is_forbidden (keyval)) { + return FALSE; + } + } + return TRUE; +} + +gboolean +is_empty_binding (const CcKeyCombo *combo) +{ + if (combo->keyval == 0 && + combo->mask == 0 && + combo->keycode == 0) + return TRUE; + return FALSE; +} + +gboolean +is_valid_accel (const CcKeyCombo *combo) +{ + /* Unlike gtk_accelerator_valid(), we want to allow Tab when combined + * with some modifiers (Alt+Tab and friends) + */ + return gtk_accelerator_valid (combo->keyval, combo->mask) || + (combo->keyval == GDK_KEY_Tab && combo->mask != 0); +} + +gchar* +find_free_settings_path (GSettings *settings) +{ + g_auto(GStrv) used_names = NULL; + g_autofree gchar *dir = NULL; + int i, num, n_names; + + used_names = g_settings_get_strv (settings, "custom-keybindings"); + n_names = g_strv_length (used_names); + + for (num = 0; dir == NULL; num++) + { + g_autofree gchar *tmp = NULL; + gboolean found = FALSE; + + tmp = g_strdup_printf ("%s/custom%d/", CUSTOM_KEYS_BASENAME, num); + for (i = 0; i < n_names && !found; i++) + found = strcmp (used_names[i], tmp) == 0; + + if (!found) + dir = g_steal_pointer (&tmp); + } + + return g_steal_pointer (&dir); +} + +KeyList* +parse_keylist_from_file (const gchar *path) +{ + KeyList *keylist; + g_autoptr(GError) err = NULL; + g_autofree gchar *buf = NULL; + gsize buf_len; + guint i; + + g_autoptr(GMarkupParseContext) ctx = NULL; + GMarkupParser parser = { parse_start_tag, NULL, NULL, NULL, NULL }; + + /* Parse file */ + if (!g_file_get_contents (path, &buf, &buf_len, &err)) + return NULL; + + keylist = g_new0 (KeyList, 1); + keylist->entries = g_array_new (FALSE, TRUE, sizeof (KeyListEntry)); + ctx = g_markup_parse_context_new (&parser, 0, keylist, NULL); + + if (!g_markup_parse_context_parse (ctx, buf, buf_len, &err)) + { + g_warning ("Failed to parse '%s': '%s'", path, err->message); + g_free (keylist->name); + g_free (keylist->package); + g_free (keylist->wm_name); + + for (i = 0; i < keylist->entries->len; i++) + g_free (((KeyListEntry *) &(keylist->entries->data[i]))->name); + + g_array_free (keylist->entries, TRUE); + g_free (keylist); + return NULL; + } + + return keylist; +} + +/* + * Stolen from GtkCellRendererAccel: + * https://git.gnome.org/browse/gtk+/tree/gtk/gtkcellrendereraccel.c#n261 + */ +gchar* +convert_keysym_state_to_string (const CcKeyCombo *combo) +{ + gchar *name; + + if (combo->keyval == 0 && combo->keycode == 0) + { + /* This label is displayed in a treeview cell displaying + * a disabled accelerator key combination. + */ + name = g_strdup (_("Disabled")); + } + else + { + name = gtk_accelerator_get_label_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask); + + if (name == NULL) + name = gtk_accelerator_name_with_keycode (NULL, combo->keyval, combo->keycode, combo->mask); + } + + return name; +} + +/* This adjusts the keyval and modifiers such that it matches how + * gnome-shell detects shortcuts, which works as follows: + * First for the non-modifier key, the keycode that generates this + * keyval at the lowest shift level is determined, which might be a + * level > 0, such as for numbers in the num-row in AZERTY. + * Next it checks if all the specified modifiers were pressed. + */ +void +normalize_keyval_and_mask (guint keycode, + GdkModifierType mask, + guint group, + guint *out_keyval, + GdkModifierType *out_mask) +{ + guint unmodified_keyval; + guint shifted_keyval; + GdkModifierType explicit_modifiers; + GdkModifierType used_modifiers; + + /* We want shift to always be included as explicit modifier for + * gnome-shell shortcuts. That's because users usually think of + * shortcuts as including the shift key rather than being defined + * for the shifted keyval. + * This helps with num-row keys which have different keyvals on + * different layouts for example, but also with keys that have + * explicit key codes at shift level 0, that gnome-shell would prefer + * over shifted ones, such the DOLLAR key. + */ + explicit_modifiers = gtk_accelerator_get_default_mod_mask () | GDK_SHIFT_MASK; + used_modifiers = mask & explicit_modifiers; + + /* Find the base keyval of the pressed key without the explicit + * modifiers. */ + gdk_display_translate_key (gdk_display_get_default (), + keycode, + mask & ~explicit_modifiers, + group, + &unmodified_keyval, + NULL, + NULL, + NULL); + + /* Normalize num-row keys to the number value. This allows these + * shortcuts to work when switching between AZERTY and layouts where + * the numbers are at shift level 0. */ + gdk_display_translate_key (gdk_display_get_default (), + keycode, + GDK_SHIFT_MASK | (mask & ~explicit_modifiers), + group, + &shifted_keyval, + NULL, + NULL, + NULL); + + if (shifted_keyval >= GDK_KEY_0 && shifted_keyval <= GDK_KEY_9) + unmodified_keyval = shifted_keyval; + + /* Normalise */ + if (unmodified_keyval == GDK_KEY_ISO_Left_Tab) + unmodified_keyval = GDK_KEY_Tab; + + if (unmodified_keyval == GDK_KEY_Sys_Req && (used_modifiers & GDK_ALT_MASK) != 0) + { + /* HACK: we don't want to use SysRq as a keybinding (but we do + * want Alt+Print), so we avoid translation from Alt+Print to SysRq */ + unmodified_keyval = GDK_KEY_Print; + } + + *out_keyval = unmodified_keyval; + *out_mask = used_modifiers; +} 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 . + * + * Authors: Thomas Wood + * Rodrigo Moya + */ + +#include +#include + +#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 @@ + + + + enter-keyboard-shortcut.svg + cc-xkb-modifier-dialog.ui + cc-keyboard-shortcut-row.ui + cc-keyboard-shortcut-dialog.ui + cc-keyboard-panel.ui + cc-keyboard-shortcut-editor.ui + cc-input-chooser.ui + cc-input-row.ui + cc-input-list-box.ui + + 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') -- cgit v1.2.3