keyboard: Move keyboard shortcuts configuration to a dialog window

This commit is contained in:
Ian Douglas Scott 2020-07-09 14:54:20 -07:00
parent ac30be8c6d
commit 238327e0ae
9 changed files with 1201 additions and 637 deletions

View file

@ -1,6 +1,8 @@
/*
/* 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
@ -17,59 +19,30 @@
*
* Author: Thomas Wood <thomas.wood@intel.com>
* Georges Basile Stavracas Neto <gbsneto@gnome.org>
* Ian Douglas Scott <idscott@system76.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <glib/gi18n.h>
#include "cc-keyboard-shortcut-row.h"
#include "cc-keyboard-item.h"
#include "cc-keyboard-manager.h"
#include "cc-keyboard-panel.h"
#include "cc-keyboard-resources.h"
#include "cc-keyboard-shortcut-editor.h"
#include "cc-keyboard-shortcut-dialog.h"
#include "cc-xkb-modifier-dialog.h"
#include "keyboard-shortcuts.h"
#include "cc-util.h"
#define SHORTCUT_DELIMITERS "+ "
typedef struct {
CcKeyboardItem *item;
gchar *section_title;
gchar *section_id;
} RowData;
struct _CcKeyboardPanel
{
CcPanel parent_instance;
/* Search */
GtkWidget *empty_search_placeholder;
GtkWidget *reset_button;
GtkWidget *search_bar;
GtkWidget *search_button;
GtkWidget *search_entry;
guint search_bar_handler_id;
/* Shortcuts */
GtkWidget *shortcuts_listbox;
GtkListBoxRow *add_shortcut_row;
GtkSizeGroup *accelerator_sizegroup;
/* Alternate characters key */
CcXkbModifierDialog *xkb_modifier_dialog;
GSettings *input_source_settings;
GtkWidget *value_alternate_chars;
/* Custom shortcut dialog */
GtkWidget *shortcut_editor;
GRegex *pictures_regex;
CcKeyboardManager *manager;
GtkListBoxRow *common_shortcuts_row;
};
CC_PANEL_REGISTER (CcKeyboardPanel, cc_keyboard_panel)
@ -79,11 +52,6 @@ enum {
PROP_PARAMETERS
};
static const gchar* custom_css =
"button.reset-shortcut-button {"
" padding: 0;"
"}";
static const XkbModifier LV3_MODIFIER = {
"lv3:",
N_("Alternate Characters Key"),
@ -100,409 +68,6 @@ static const XkbModifier LV3_MODIFIER = {
"lv3:ralt_switch",
};
/* RowData functions */
static RowData *
row_data_new (CcKeyboardItem *item,
const gchar *section_id,
const gchar *section_title)
{
RowData *data;
data = g_new0 (RowData, 1);
data->item = g_object_ref (item);
data->section_id = g_strdup (section_id);
data->section_title = g_strdup (section_title);
return data;
}
static void
row_data_free (RowData *data)
{
g_object_unref (data->item);
g_free (data->section_id);
g_free (data->section_title);
g_free (data);
}
static void
reset_all_shortcuts_cb (GtkWidget *widget,
gpointer user_data)
{
CcKeyboardPanel *self;
RowData *data;
self = user_data;
if (widget == (GtkWidget *) self->add_shortcut_row)
return;
data = g_object_get_data (G_OBJECT (widget), "data");
/* Don't reset custom shortcuts */
if (cc_keyboard_item_get_item_type (data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
return;
/* cc_keyboard_manager_reset_shortcut() already resets conflicting shortcuts,
* so no other check is needed here. */
cc_keyboard_manager_reset_shortcut (self->manager, data->item);
}
static void
reset_all_clicked_cb (CcKeyboardPanel *self)
{
GtkWidget *dialog, *toplevel, *button;
guint response;
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
dialog = gtk_message_dialog_new (GTK_WINDOW (toplevel),
GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_WARNING,
GTK_BUTTONS_NONE,
_("Reset All Shortcuts?"));
gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
_("Resetting the shortcuts may affect your custom shortcuts. "
"This cannot be undone."));
gtk_dialog_add_buttons (GTK_DIALOG (dialog),
_("Cancel"), GTK_RESPONSE_CANCEL,
_("Reset All"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
/* Make the "Reset All" button destructive */
button = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
gtk_style_context_add_class (gtk_widget_get_style_context (button), "destructive-action");
/* Reset shortcuts if accepted */
response = gtk_dialog_run (GTK_DIALOG (dialog));
if (response == GTK_RESPONSE_ACCEPT)
{
gtk_container_foreach (GTK_CONTAINER (self->shortcuts_listbox),
reset_all_shortcuts_cb,
self);
}
gtk_widget_destroy (dialog);
}
static void
add_item (CcKeyboardPanel *self,
CcKeyboardItem *item,
const gchar *section_id,
const gchar *section_title)
{
GtkWidget *row;
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",
row_data_new (item, section_id, section_title),
(GDestroyNotify) row_data_free);
gtk_container_add (GTK_CONTAINER (self->shortcuts_listbox), row);
}
static void
remove_item (CcKeyboardPanel *self,
CcKeyboardItem *item)
{
GList *children, *l;
children = gtk_container_get_children (GTK_CONTAINER (self->shortcuts_listbox));
for (l = children; l != NULL; l = l->next)
{
RowData *row_data;
row_data = g_object_get_data (l->data, "data");
if (row_data->item == item)
{
gtk_container_remove (GTK_CONTAINER (self->shortcuts_listbox), l->data);
break;
}
}
g_list_free (children);
}
static gboolean
strv_contains_prefix_or_match (gchar **strv,
const gchar *prefix)
{
guint i;
const struct {
const gchar *key;
const gchar *untranslated;
const gchar *synonym;
} key_aliases[] =
{
{ "ctrl", "Ctrl", "ctrl" },
{ "win", "Super", "super" },
{ "option", NULL, "alt" },
{ "command", NULL, "super" },
{ "apple", NULL, "super" },
};
for (i = 0; strv[i]; i++)
{
if (g_str_has_prefix (strv[i], prefix))
return TRUE;
}
for (i = 0; i < G_N_ELEMENTS (key_aliases); i++)
{
g_autofree gchar *alias = NULL;
const gchar *synonym;
if (!g_str_has_prefix (key_aliases[i].key, prefix))
continue;
if (key_aliases[i].untranslated)
{
const gchar *translated_label;
/* Steal GTK+'s translation */
translated_label = g_dpgettext2 ("gtk30", "keyboard label", key_aliases[i].untranslated);
alias = g_utf8_strdown (translated_label, -1);
}
synonym = key_aliases[i].synonym;
/* If a translation or synonym of the key is in the accelerator, and we typed
* the key, also consider that a prefix */
if ((alias && g_strv_contains ((const gchar * const *) strv, alias)) ||
(synonym && g_strv_contains ((const gchar * const *) strv, synonym)))
{
return TRUE;
}
}
return FALSE;
}
static gboolean
search_match_shortcut (CcKeyboardItem *item,
const gchar *search)
{
GStrv shortcut_tokens, search_tokens;
g_autofree gchar *normalized_accel = NULL;
g_autofree gchar *accel = NULL;
gboolean match;
guint i;
GList *key_combos, *l;
CcKeyCombo *combo;
key_combos = cc_keyboard_item_get_key_combos (item);
for (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 (i = 0; search_tokens[i] != NULL; i++)
{
const gchar *token;
/* Strip leading and trailing whitespaces */
token = g_strstrip (search_tokens[i]);
if (g_utf8_strlen (token, -1) == 0)
continue;
match = match && strv_contains_prefix_or_match (shortcut_tokens, token);
if (!match)
break;
}
g_strfreev (shortcut_tokens);
g_strfreev (search_tokens);
if (match)
return TRUE;
}
return FALSE;
}
static gint
sort_function (GtkListBoxRow *a,
GtkListBoxRow *b,
gpointer user_data)
{
CcKeyboardPanel *self;
RowData *a_data, *b_data;
gint retval;
self = user_data;
if (a == self->add_shortcut_row)
return 1;
if (b == self->add_shortcut_row)
return -1;
a_data = g_object_get_data (G_OBJECT (a), "data");
b_data = g_object_get_data (G_OBJECT (b), "data");
/* Put custom shortcuts below everything else */
if (cc_keyboard_item_get_item_type (a_data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
return 1;
else if (cc_keyboard_item_get_item_type (b_data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
return -1;
retval = g_strcmp0 (a_data->section_title, b_data->section_title);
if (retval != 0)
return retval;
return g_strcmp0 (cc_keyboard_item_get_description (a_data->item), cc_keyboard_item_get_description (b_data->item));
}
static void
header_function (GtkListBoxRow *row,
GtkListBoxRow *before,
gpointer user_data)
{
CcKeyboardPanel *self;
gboolean add_header;
RowData *data;
self = user_data;
add_header = FALSE;
/* The + row always has a separator */
if (row == self->add_shortcut_row)
{
GtkWidget *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_show (separator);
gtk_list_box_row_set_header (row, separator);
return;
}
data = g_object_get_data (G_OBJECT (row), "data");
if (before)
{
RowData *before_data = g_object_get_data (G_OBJECT (before), "data");
if (before_data)
add_header = g_strcmp0 (before_data->section_id, data->section_id) != 0;
}
else
{
add_header = TRUE;
}
if (add_header)
{
GtkWidget *box, *label, *separator;
g_autofree gchar *markup = NULL;
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_show (box);
gtk_widget_set_margin_top (box, before ? 18 : 6);
markup = g_strdup_printf ("<b>%s</b>", _(data->section_title));
label = gtk_label_new (NULL);
gtk_label_set_markup (GTK_LABEL (label), markup);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_widget_set_margin_start (label, 6);
gtk_widget_show (label);
gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
gtk_container_add (GTK_CONTAINER (box), label);
separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_show (separator);
gtk_container_add (GTK_CONTAINER (box), separator);
gtk_list_box_row_set_header (row, box);
}
else
{
gtk_list_box_row_set_header (row, NULL);
}
}
static gboolean
filter_function (GtkListBoxRow *row,
gpointer user_data)
{
CcKeyboardPanel *self = user_data;
CcKeyboardItem *item;
RowData *data;
gboolean retval;
g_autofree gchar *search = NULL;
g_autofree gchar *name = NULL;
g_auto(GStrv) terms = NULL;
guint i;
if (gtk_entry_get_text_length (GTK_ENTRY (self->search_entry)) == 0)
return TRUE;
/* When searching, the '+' row is always hidden */
if (row == self->add_shortcut_row)
return FALSE;
data = g_object_get_data (G_OBJECT (row), "data");
item = data->item;
name = cc_util_normalize_casefold_and_unaccent (cc_keyboard_item_get_description (item));
search = cc_util_normalize_casefold_and_unaccent (gtk_entry_get_text (GTK_ENTRY (self->search_entry)));
terms = g_strsplit (search, " ", -1);
for (i = 0; terms && terms[i]; i++)
{
retval = strstr (name, terms[i]) || search_match_shortcut (item, terms[i]);
if (!retval)
break;
}
return retval;
}
static void
shortcut_row_activated (GtkWidget *button,
GtkListBoxRow *row,
CcKeyboardPanel *self)
{
CcKeyboardShortcutEditor *editor;
editor = CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor);
if (row != self->add_shortcut_row)
{
RowData *data = g_object_get_data (G_OBJECT (row), "data");
cc_keyboard_shortcut_editor_set_mode (editor, CC_SHORTCUT_EDITOR_EDIT);
cc_keyboard_shortcut_editor_set_item (editor, data->item);
}
else
{
cc_keyboard_shortcut_editor_set_mode (editor, CC_SHORTCUT_EDITOR_CREATE);
cc_keyboard_shortcut_editor_set_item (editor, NULL);
}
gtk_widget_show (self->shortcut_editor);
}
static void
alternate_chars_activated (GtkWidget *button,
GtkListBoxRow *row,
@ -516,6 +81,24 @@ alternate_chars_activated (GtkWidget *button,
gtk_widget_show (GTK_WIDGET (self->xkb_modifier_dialog));
}
static void
keyboard_shortcuts_activated (GtkWidget *button,
GtkListBoxRow *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,
@ -542,45 +125,12 @@ static void
cc_keyboard_panel_finalize (GObject *object)
{
CcKeyboardPanel *self = CC_KEYBOARD_PANEL (object);
GtkWidget *window;
g_clear_pointer (&self->pictures_regex, g_regex_unref);
g_clear_object (&self->accelerator_sizegroup);
g_clear_object (&self->input_source_settings);
if (self->search_bar_handler_id != 0)
{
window = cc_shell_get_toplevel (cc_panel_get_shell (CC_PANEL (self)));
g_signal_handler_disconnect (window, self->search_bar_handler_id);
}
G_OBJECT_CLASS (cc_keyboard_panel_parent_class)->finalize (object);
}
static void
cc_keyboard_panel_constructed (GObject *object)
{
CcKeyboardPanel *self = CC_KEYBOARD_PANEL (object);
GtkWindow *toplevel;
CcShell *shell;
G_OBJECT_CLASS (cc_keyboard_panel_parent_class)->constructed (object);
/* Setup the dialog's transient parent */
shell = cc_panel_get_shell (CC_PANEL (self));
toplevel = GTK_WINDOW (cc_shell_get_toplevel (shell));
gtk_window_set_transient_for (GTK_WINDOW (self->shortcut_editor), toplevel);
cc_shell_embed_widget_in_header (shell, self->reset_button, GTK_POS_LEFT);
cc_shell_embed_widget_in_header (shell, self->search_button, GTK_POS_RIGHT);
self->search_bar_handler_id =
g_signal_connect_swapped (toplevel,
"key-press-event",
G_CALLBACK (gtk_search_bar_handle_event),
self->search_bar);
}
static void
cc_keyboard_panel_class_init (CcKeyboardPanelClass *klass)
{
@ -592,45 +142,25 @@ cc_keyboard_panel_class_init (CcKeyboardPanelClass *klass)
object_class->set_property = cc_keyboard_panel_set_property;
object_class->finalize = cc_keyboard_panel_finalize;
object_class->constructed = cc_keyboard_panel_constructed;
g_object_class_override_property (object_class, PROP_PARAMETERS, "parameters");
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-keyboard-panel.ui");
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, add_shortcut_row);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, empty_search_placeholder);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, reset_button);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, search_bar);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, search_button);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, search_entry);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, shortcuts_listbox);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, value_alternate_chars);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardPanel, common_shortcuts_row);
gtk_widget_class_bind_template_callback (widget_class, reset_all_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, shortcut_row_activated);
gtk_widget_class_bind_template_callback (widget_class, alternate_chars_activated);
gtk_widget_class_bind_template_callback (widget_class, keyboard_shortcuts_activated);
}
static void
cc_keyboard_panel_init (CcKeyboardPanel *self)
{
GtkCssProvider *provider;
g_resources_register (cc_keyboard_get_resource ());
gtk_widget_init_template (GTK_WIDGET (self));
/* Custom CSS */
provider = gtk_css_provider_new ();
gtk_css_provider_load_from_data (provider, custom_css, -1, NULL);
gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
GTK_STYLE_PROVIDER (provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1);
g_object_unref (provider);
/* Alternate characters key */
self->input_source_settings = g_settings_new ("org.gnome.desktop.input-sources");
g_settings_bind_with_mapping (self->input_source_settings,
@ -644,46 +174,4 @@ cc_keyboard_panel_init (CcKeyboardPanel *self)
NULL);
self->xkb_modifier_dialog = cc_xkb_modifier_dialog_new (self->input_source_settings, &LV3_MODIFIER);
/* Shortcut manager */
self->manager = cc_keyboard_manager_new ();
/* Shortcut editor dialog */
self->shortcut_editor = cc_keyboard_shortcut_editor_new (self->manager);
/* Use a sizegroup to make the accelerator labels the same width */
self->accelerator_sizegroup = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
g_signal_connect_object (self->manager,
"shortcut-added",
G_CALLBACK (add_item),
self,
G_CONNECT_SWAPPED);
g_signal_connect_object (self->manager,
"shortcut-removed",
G_CALLBACK (remove_item),
self,
G_CONNECT_SWAPPED);
cc_keyboard_manager_load_shortcuts (self->manager);
/* Setup the shortcuts shortcuts_listbox */
gtk_list_box_set_sort_func (GTK_LIST_BOX (self->shortcuts_listbox),
sort_function,
self,
NULL);
gtk_list_box_set_header_func (GTK_LIST_BOX (self->shortcuts_listbox),
header_function,
self,
NULL);
gtk_list_box_set_filter_func (GTK_LIST_BOX (self->shortcuts_listbox),
filter_function,
self,
NULL);
gtk_list_box_set_placeholder (GTK_LIST_BOX (self->shortcuts_listbox), self->empty_search_placeholder);
}

View file

@ -29,6 +29,4 @@ G_BEGIN_DECLS
#define CC_TYPE_KEYBOARD_PANEL (cc_keyboard_panel_get_type ())
G_DECLARE_FINAL_TYPE (CcKeyboardPanel, cc_keyboard_panel, CC, KEYBOARD_PANEL, CcPanel)
CcKeyboardItem* cc_keyboard_panel_create_custom_item (CcKeyboardPanel *self);
G_END_DECLS

View file

@ -12,26 +12,11 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="expand">True</property>
<signal name="key-press-event" handler="gtk_search_bar_handle_event" object="search_bar" swapped="yes" />
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSearchBar" id="search_bar">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="search_mode_enabled" bind-source="search_button" bind-property="active" bind-flags="bidirectional" />
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="width_chars">30</property>
<signal name="notify::text" handler="gtk_list_box_invalidate_filter" object="shortcuts_listbox" swapped="yes" />
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
@ -53,6 +38,17 @@
<property name="margin_right">18</property>
<property name="spacing">12</property>
<property name="halign">center</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Type Special Characters</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
@ -68,7 +64,6 @@
<object class="HdyActionRow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="use-underline">true</property>
<property name="title" translatable="yes">Alternate Characters Key</property>
<property name="subtitle" translatable="yes">Hold down and type to enter different characters</property>
<property name="activatable">True</property>
@ -94,34 +89,41 @@
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Keyboard Shortcuts</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox" id="shortcuts_listbox">
<object class="GtkListBox" id="keyboard_shortcuts_listbox">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="selection-mode">none</property>
<property name="width-request">250</property>
<signal name="row-activated" handler="shortcut_row_activated" object="CcKeyboardPanel" swapped="no" />
<signal name="row-activated" handler="keyboard_shortcuts_activated" object="CcKeyboardPanel" swapped="no" />
<child>
<object class="GtkListBoxRow" id="add_shortcut_row">
<object class="HdyActionRow" id="common_shortcuts_row">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="title" translatable="yes">Customize Shortcuts</property>
<property name="activatable">True</property>
<child>
<object class="GtkBox">
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="border_width">6</property>
<child type="center">
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add-symbolic</property>
</object>
</child>
<property name="icon_name">go-next-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
@ -137,69 +139,4 @@
</object>
</child>
</template>
<!-- Header widgets -->
<object class="GtkToggleButton" id="search_button">
<property name="visible">True</property>
<style>
<class name="image-button" />
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon_name">system-search-symbolic</property>
</object>
</child>
</object>
<object class="GtkButton" id="reset_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label" translatable="yes">Reset All…</property>
<property name="tooltip-text" translatable="yes">Reset all shortcuts to their default keybindings</property>
<signal name="clicked" handler="reset_all_clicked_cb" object="CcKeyboardPanel" swapped="yes" />
</object>
<object class="GtkBox" id="empty_search_placeholder">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="border_width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">80</property>
<property name="icon_name">edit-find-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">No keyboard shortcut found</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.44"/>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Try a different search</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
</interface>

View file

@ -0,0 +1,847 @@
/* cc-keyboard-shortcut-dialog.c
*
* Copyright (C) 2010 Intel, Inc
* Copyright (C) 2016 Endless, Inc
* Copyright (C) 2020 System76, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*
* Author: Thomas Wood <thomas.wood@intel.com>
* Georges Basile Stavracas Neto <gbsneto@gnome.org>
* Ian Douglas Scott <idscott@system76.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <glib/gi18n.h>
#define HANDY_USE_UNSTABLE_API
#include <handy.h>
#include "cc-keyboard-shortcut-dialog.h"
#include "cc-keyboard-item.h"
#include "cc-keyboard-manager.h"
#include "cc-keyboard-shortcut-editor.h"
#include "cc-keyboard-shortcut-row.h"
#include "cc-list-row.h"
#include "cc-util.h"
#include "list-box-helper.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;
GtkRevealer *back_revealer;
GtkWidget *custom_shortcut_add_box;
guint custom_shortcut_count;
GtkWidget *empty_custom_shortcuts_placeholder;
GtkWidget *empty_search_placeholder;
GtkHeaderBar *headerbar;
GtkRevealer *reset_all_revealer;
GtkSearchEntry *search_entry;
GtkListBox *section_listbox;
GtkListBoxRow *section_row;
GtkScrolledWindow *section_scrolled_window;
GtkListBox *shortcut_listbox;
GtkScrolledWindow *shortcut_scrolled_window;
GtkStack *stack;
CcKeyboardManager *manager;
GtkWidget *shortcut_editor;
GHashTable *sections;
};
G_DEFINE_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, GTK_TYPE_DIALOG)
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, *label, *box;
GtkListBoxRow *row;
icon = g_object_new (GTK_TYPE_IMAGE,
"visible", 1,
"icon_name", "go-next-symbolic",
NULL);
gtk_style_context_add_class (gtk_widget_get_style_context (icon), "dim-label");
modified_label = g_object_new (GTK_TYPE_LABEL,
"visible", 1,
NULL);
gtk_style_context_add_class (gtk_widget_get_style_context (modified_label), "dim-label");
label = g_object_new (GTK_TYPE_LABEL,
"visible", 1,
"label", _(section_title),
NULL);
gtk_style_context_add_class (gtk_widget_get_style_context (label), "row-label");
box = g_object_new (GTK_TYPE_BOX,
"visible", 1,
"spacing", 8,
"margin_left", 12,
"margin_right", 12,
"margin_top", 8,
"margin_bottom", 8,
NULL);
gtk_container_add (GTK_CONTAINER (box), label);
gtk_box_pack_end (GTK_BOX (box), icon, FALSE, FALSE, 0);
gtk_box_pack_end (GTK_BOX (box), modified_label, FALSE, FALSE, 0);
row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
"visible", 1,
NULL);
gtk_container_add (GTK_CONTAINER (row), box);
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_container_add (GTK_CONTAINER (self->section_listbox), GTK_WIDGET (row));
return row;
}
static void
set_custom_shortcut_add_box_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, GTK_WIDGET (self->shortcut_scrolled_window));
}
gtk_widget_set_visible (self->custom_shortcut_add_box, is_custom_shortcuts);
}
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_add_box_visibility (self);
}
gtk_container_add (GTK_CONTAINER (self->shortcut_listbox), row);
}
static void
remove_item (CcKeyboardShortcutDialog *self,
CcKeyboardItem *item)
{
g_autoptr(GList) children;
children = gtk_container_get_children (GTK_CONTAINER (self->shortcut_listbox));
for (GList *l = children; l != NULL; l = l->next)
{
ShortcutRowData *row_data;
row_data = g_object_get_data (l->data, "data");
if (row_data->item == item)
{
if (strcmp (row_data->section_id, "custom") == 0)
{
self->custom_shortcut_count--;
set_custom_shortcut_add_box_visibility (self);
}
gtk_container_remove (GTK_CONTAINER (self->shortcut_listbox), l->data);
break;
}
}
}
static void
update_modified_counts (CcKeyboardShortcutDialog *self)
{
g_autoptr(GList) sections = NULL, shortcuts = NULL;
SectionRowData *section_data;
ShortcutRowData *shortcut_data;
g_autofree gchar *modified_text = NULL;
sections = gtk_container_get_children (GTK_CONTAINER (self->section_listbox));
shortcuts = gtk_container_get_children (GTK_CONTAINER (self->shortcut_listbox));
for (GList *l = sections; l != NULL; l = l->next)
{
section_data = g_object_get_data (G_OBJECT (l->data), "data");
section_data->modified_count = 0;
}
for (GList *l = shortcuts; l != NULL; l = l->next)
{
shortcut_data = g_object_get_data (G_OBJECT (l->data), "data");
if (!cc_keyboard_item_is_value_default (shortcut_data->item))
shortcut_data->section_data->modified_count++;
}
for (GList *l = sections; l != NULL; l = l->next)
{
section_data = g_object_get_data (G_OBJECT (l->data), "data");
if (section_data->modified_count > 0)
{
modified_text = g_strdup_printf ("%d %s", section_data->modified_count, _("modified"));
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, GTK_WIDGET (self->section_scrolled_window));
gtk_header_bar_set_title (self->headerbar, _("Keyboard Shortcuts"));
gtk_entry_set_text(GTK_ENTRY (self->search_entry), "");
gtk_revealer_set_reveal_child (self->reset_all_revealer, TRUE);
gtk_revealer_set_reveal_child (self->back_revealer, FALSE);
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);
}
if (is_custom_shortcuts)
gtk_list_box_set_placeholder (self->shortcut_listbox, NULL);
else
gtk_list_box_set_placeholder (self->shortcut_listbox, self->empty_search_placeholder);
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, GTK_WIDGET (self->shortcut_scrolled_window));
gtk_header_bar_set_title (self->headerbar, title);
set_custom_shortcut_add_box_visibility (self);
gtk_revealer_set_reveal_child (self->reset_all_revealer, FALSE);
gtk_revealer_set_reveal_child (self->back_revealer, TRUE);
gtk_widget_set_visible (GTK_WIDGET (self->search_entry), self->section_row == NULL);
}
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;
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
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
back_button_clicked_cb (CcKeyboardShortcutDialog *self)
{
show_section_list (self);
}
static void
reset_all_shortcuts_cb (GtkWidget *widget,
gpointer user_data)
{
CcKeyboardShortcutDialog *self;
ShortcutRowData *data;
self = user_data;
data = g_object_get_data (G_OBJECT (widget), "data");
/* Don't reset custom shortcuts */
if (cc_keyboard_item_get_item_type (data->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
return;
/* cc_keyboard_manager_reset_shortcut() already resets conflicting shortcuts,
* so no other check is needed here. */
cc_keyboard_manager_reset_shortcut (self->manager, data->item);
}
static void
reset_all_clicked_cb (CcKeyboardShortcutDialog *self)
{
GtkWidget *dialog, *toplevel, *button;
guint response;
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
dialog = gtk_message_dialog_new (GTK_WINDOW (toplevel),
GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_WARNING,
GTK_BUTTONS_NONE,
_("Reset All Shortcuts?"));
gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
_("Resetting the shortcuts may affect your custom shortcuts. "
"This cannot be undone."));
gtk_dialog_add_buttons (GTK_DIALOG (dialog),
_("Cancel"), GTK_RESPONSE_CANCEL,
_("Reset All"), GTK_RESPONSE_ACCEPT,
NULL);
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
/* Make the "Reset All" button destructive */
button = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
gtk_style_context_add_class (gtk_widget_get_style_context (button), "destructive-action");
/* Reset shortcuts if accepted */
response = gtk_dialog_run (GTK_DIALOG (dialog));
if (response == GTK_RESPONSE_ACCEPT)
{
gtk_container_foreach (GTK_CONTAINER (self->shortcut_listbox),
reset_all_shortcuts_cb,
self);
}
gtk_widget_destroy (dialog);
update_modified_counts (self);
}
static void
search_entry_cb (CcKeyboardShortcutDialog *self)
{
if (gtk_entry_get_text_length (GTK_ENTRY (self->search_entry)) == 0 && self->section_row == NULL)
show_section_list (self);
else if (gtk_stack_get_visible_child (self->stack) != GTK_WIDGET (self->shortcut_scrolled_window))
show_shortcut_list (self);
else
gtk_list_box_invalidate_filter (self->shortcut_listbox);
}
static void
key_press_cb (CcKeyboardShortcutDialog *self, GdkEvent *event)
{
if (gtk_widget_get_visible (GTK_WIDGET (self->search_entry)))
gtk_search_entry_handle_event (self->search_entry, event);
}
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 ("gtk30", "keyboard label", key_aliases[i].untranslated);
alias = g_utf8_strdown (translated_label, -1);
}
synonym = key_aliases[i].synonym;
/* If a translation or synonym of the key is in the accelerator, and we typed
* the key, also consider that a prefix */
if ((alias && g_strv_contains ((const gchar * const *) strv, alias)) ||
(synonym && g_strv_contains ((const gchar * const *) strv, synonym)))
{
return TRUE;
}
}
return FALSE;
}
static gboolean
search_match_shortcut (CcKeyboardItem *item,
const gchar *search)
{
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)
{
ShortcutRowData *a_data, *b_data;
gint retval;
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;
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;
if (self->section_row != NULL)
{
section_data = g_object_get_data (G_OBJECT (self->section_row), "data");
data = g_object_get_data (G_OBJECT (row), "data");
if (strcmp (data->section_id, section_data->section_id) != 0)
return FALSE;
}
if (gtk_entry_get_text_length (GTK_ENTRY (self->search_entry)) == 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 (gtk_entry_get_text (GTK_ENTRY (self->search_entry)));
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 void
shortcut_header_function (GtkListBoxRow *row,
GtkListBoxRow *before,
gpointer user_data)
{
CcKeyboardShortcutDialog *self;
gboolean add_header;
ShortcutRowData *data, *before_data;
data = g_object_get_data (G_OBJECT (row), "data");
self = user_data;
add_header = FALSE;
if (before)
{
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);
gtk_widget_show (box);
if (!before)
gtk_widget_set_margin_top (box, 6);
if (before)
{
separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_show (separator);
gtk_container_add (GTK_CONTAINER (box), separator);
}
markup = g_strdup_printf ("<b>%s</b>", _(data->section_title));
label = g_object_new (GTK_TYPE_LABEL,
"label", markup,
"use-markup", TRUE,
"xalign", 0.0,
"margin-start", 6,
NULL);
gtk_widget_show (label);
gtk_container_add (GTK_CONTAINER (box), label);
separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_show (separator);
gtk_container_add (GTK_CONTAINER (box), separator);
gtk_list_box_row_set_header (row, box);
}
else if (before)
{
GtkWidget *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_show (separator);
gtk_list_box_row_set_header (row, separator);
}
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_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;
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_revealer);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, custom_shortcut_add_box);
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, reset_all_revealer);
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_scrolled_window);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_listbox);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_scrolled_window);
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, key_press_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));
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_header_func (self->section_listbox, cc_list_box_update_header_func, NULL, NULL);
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);
}

View file

@ -0,0 +1,35 @@
/* cc-keyboard-shortcut-dialog.h
*
* Copyright (C) 2020 System76, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*
* Author: Ian Douglas Scott <idscott@system76.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <gtk/gtk.h>
G_BEGIN_DECLS
#define CC_TYPE_KEYBOARD_SHORTCUT_DIALOG (cc_keyboard_shortcut_dialog_get_type ())
G_DECLARE_FINAL_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, CC, KEYBOARD_SHORTCUT_DIALOG, GtkDialog)
GtkWidget* cc_keyboard_shortcut_dialog_new (void);
G_END_DECLS

View file

@ -0,0 +1,257 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<template class="CcKeyboardShortcutDialog" parent="GtkDialog">
<property name="modal">True</property>
<signal name="key-press-event" handler="key_press_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
<child internal-child="vbox">
<object class="GtkBox">
<property name="visible">True</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<property name="visible">True</property>
<property name="margin-top">12</property>
<property name="width_chars">30</property>
<property name="halign">center</property>
<signal name="notify::text" handler="search_entry_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<child>
<object class="GtkScrolledWindow" id="section_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<property name="max_content_height">350</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="border_width">12</property>
<child>
<object class="GtkListBox" id="section_listbox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<property name="border_width">12</property>
<signal name="row-activated" handler="section_row_activated" object="CcKeyboardShortcutDialog" swapped="no" />
<style>
<class name="frame" />
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="shortcut_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">never</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<property name="max_content_height">350</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="border_width">12</property>
<property name="spacing">12</property>
<child>
<object class="GtkListBox" id="custom_shortcut_add_box">
<property name="selection-mode">none</property>
<style>
<class name="frame" />
</style>
<child>
<object class="HdyActionRow">
<property name="visible">True</property>
<property name="title" translatable="yes">Add Custom Shortcuts</property>
<property name="subtitle" translatable="yes">Set up custom shortcuts for launching apps, running scripts, and more.</property>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Add Shortcut</property>
<signal name="clicked" handler="add_custom_shortcut_clicked_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox" id="shortcut_listbox">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<style>
<class name="frame" />
</style>
<signal name="row-activated" handler="shortcut_row_activated" object="CcKeyboardShortcutDialog" swapped="no" />
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="empty_custom_shortcuts_placeholder">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="border_width">18</property>
<property name="spacing">18</property>
<property name="valign">center</property>
<style>
<class name="background"/>
</style>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon-name">input-keyboard-symbolic</property>
<property name="pixel-size">128</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Add Custom Shortcuts</property>
<attributes>
<attribute name="weight" value="bold" />
</attributes>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Set up custom shortcuts for launching apps, running scripts, and more.</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Add Shortcut</property>
<style>
<class name="suggested-action" />
</style>
<signal name="clicked" handler="add_custom_shortcut_clicked_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar" id="headerbar">
<property name="visible">True</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkRevealer" id="back_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkButton" id="back">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="valign">center</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="back_button_clicked_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
<style>
<class name="image-button"/>
</style>
<child internal-child="accessible">
<object class="AtkObject" id="a11y-back">
<property name="accessible-name" translatable="yes">Back</property>
</object>
</child>
<child>
<object class="GtkImage" id="back_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-previous-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkRevealer" id="reset_all_revealer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition-type">crossfade</property>
<child>
<object class="GtkButton" id="reset_all_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label" translatable="yes">Reset All…</property>
<property name="tooltip-text" translatable="yes">Reset all shortcuts to their default keybindings</property>
<signal name="clicked" handler="reset_all_clicked_cb" object="CcKeyboardShortcutDialog" swapped="yes" />
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
</object>
</child>
</template>
<object class="GtkBox" id="empty_search_placeholder">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="border_width">18</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">80</property>
<property name="icon_name">edit-find-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">No keyboard shortcut found</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.44"/>
</attributes>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Try a different search</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
<object class="GtkSizeGroup" id="accelerator_sizegroup" />
</interface>

View file

@ -1,5 +1,5 @@
[Desktop Entry]
Name=Keyboard Shortcuts
Name=Keyboard
Comment=View and change keyboard shortcuts and set your typing preferences
Exec=gnome-control-center keyboard
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!

View file

@ -4,6 +4,7 @@
<file preprocess="xml-stripblanks">enter-keyboard-shortcut.svg</file>
<file preprocess="xml-stripblanks">cc-xkb-modifier-dialog.ui</file>
<file preprocess="xml-stripblanks">cc-keyboard-shortcut-row.ui</file>
<file preprocess="xml-stripblanks">cc-keyboard-shortcut-dialog.ui</file>
<file preprocess="xml-stripblanks">cc-keyboard-panel.ui</file>
<file preprocess="xml-stripblanks">cc-keyboard-shortcut-editor.ui</file>
</gresource>

View file

@ -58,6 +58,7 @@ 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',