gnome-control-center/panels/keyboard/cc-keyboard-shortcut-dialog.c
Maximiliano Sandoval R 526217badb keyboard-shortcut-dialog: Allow escape to close
At the moment the search entry is consuming the Escape key press, so we
have to manually tell it what to do with it.

We clear the search on the first press, if there is input, and close the
window otherwise.
2023-10-20 10:55:01 +00:00

601 lines
21 KiB
C

/* cc-keyboard-shortcut-dialog.c
*
* Copyright (C) 2010 Intel, Inc
* Copyright (C) 2016 Endless, Inc
* Copyright (C) 2020 System76, Inc.
* Copyright (C) 2022 Purism SPC
*
* 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>
* Mohammed Sadiq <sadiq@sadiqpk.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config.h>
#include <glib/gi18n.h>
#include <adwaita.h>
#include "cc-keyboard-shortcut-dialog.h"
#include "cc-keyboard-item.h"
#include "cc-keyboard-manager.h"
#include "cc-keyboard-shortcut-editor.h"
#include "cc-keyboard-shortcut-group.h"
#include "cc-keyboard-shortcut-row.h"
#include "cc-list-row.h"
#include "cc-util.h"
#include "keyboard-shortcuts.h"
struct _CcKeyboardShortcutDialog
{
AdwWindow parent_instance;
AdwNavigationView *navigation_view;
AdwNavigationPage *main_page;
GtkButton *reset_all_button;
GtkSearchEntry *search_entry;
GtkStack *section_stack;
AdwPreferencesPage *section_list_page;
GtkListBox *section_list_box;
AdwPreferencesPage *search_result_page;
AdwStatusPage *empty_results_page;
AdwNavigationPage *subview_page;
GtkStack *subview_stack;
GtkStack *shortcut_list_stack;
AdwStatusPage *empty_custom_shortcut_page;
GtkSizeGroup *accelerator_size_group;
/* A GListStore of sections containing a GListStore of CcKeyboardItem */
GListStore *sections;
GListStore *visible_section;
GtkFlattenListModel *filtered_shortcuts;
CcKeyboardManager *manager;
GtkWidget *shortcut_editor;
GStrv search_terms;
};
G_DEFINE_TYPE (CcKeyboardShortcutDialog, cc_keyboard_shortcut_dialog, ADW_TYPE_WINDOW)
static GListStore *
keyboard_shortcut_get_section_store (CcKeyboardShortcutDialog *self,
const char *section_id,
const char *section_title)
{
g_autoptr(GListStore) section = NULL;
GtkWidget *group;
guint n_items;
g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
g_assert (section_id && *section_id);
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
for (guint i = 0; i < n_items; i++)
{
g_autoptr(GObject) item = NULL;
const char *item_section_id;
item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
item_section_id = g_object_get_data (item, "id");
if (g_str_equal (item_section_id, section_id))
return G_LIST_STORE (item);
}
/* Found no matching section, so create one */
section = g_list_store_new (CC_TYPE_KEYBOARD_ITEM);
g_object_set_data_full (G_OBJECT (section), "id", g_strdup (section_id), g_free);
g_object_set_data_full (G_OBJECT (section), "title", g_strdup (section_title), g_free);
/* This group shall be shown in the search results page */
group = cc_keyboard_shortcut_group_new (G_LIST_MODEL (section),
section_id, section_title,
self->manager,
CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
self->accelerator_size_group);
g_object_set_data (G_OBJECT (section), "search-group", group);
/* This group shall be shown when a section title row is activated */
group = cc_keyboard_shortcut_group_new (G_LIST_MODEL (section),
section_id, NULL,
self->manager,
CC_KEYBOARD_SHORTCUT_EDITOR (self->shortcut_editor),
self->accelerator_size_group);
g_object_set_data (G_OBJECT (section), "group", group);
g_list_store_append (self->sections, section);
return section;
}
static void
shortcut_added_cb (CcKeyboardShortcutDialog *self,
CcKeyboardItem *item,
const char *section_id,
const char *section_title)
{
GListStore *section;
section = keyboard_shortcut_get_section_store (self, section_id, section_title);
g_object_set_data (G_OBJECT (item), "section", section);
g_list_store_append (section, item);
}
static void
shortcut_removed_cb (CcKeyboardShortcutDialog *self,
CcKeyboardItem *item)
{
GListStore *section;
guint position;
section = g_object_get_data (G_OBJECT (item), "section");
g_return_if_fail (section);
if (g_list_store_find (section, item, &position))
g_list_store_remove (section, position);
}
static void
shortuct_custom_items_changed (CcKeyboardShortcutDialog *self)
{
GListStore *section;
GtkWidget *page;
g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
section = keyboard_shortcut_get_section_store (self, "custom", "Custom Shortcuts");
if (self->visible_section == section)
{
guint n_items;
n_items = g_list_model_get_n_items (G_LIST_MODEL (section));
if (n_items)
page = GTK_WIDGET (self->shortcut_list_stack);
else
page = GTK_WIDGET (self->empty_custom_shortcut_page);
}
else
page = GTK_WIDGET (self->shortcut_list_stack);
gtk_stack_set_visible_child (self->subview_stack, page);
}
static int
compare_sections_title (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
GObject *obj_a, *obj_b;
const char *title_a, *title_b, *id_a, *id_b;
obj_a = G_OBJECT (a);
obj_b = G_OBJECT (b);
id_a = g_object_get_data (obj_a, "id");
id_b = g_object_get_data (obj_b, "id");
/* Always place custom row as the last item */
if (g_str_equal (id_a, "custom"))
return 1;
if (g_str_equal (id_b, "custom"))
return -1;
title_a = _(g_object_get_data (obj_a, "title"));
title_b = _(g_object_get_data (obj_b, "title"));
return g_strcmp0 (title_a, title_b);
}
static void
shortcut_search_result_changed_cb (CcKeyboardShortcutDialog *self)
{
GListModel *model;
GtkWidget *page;
guint n_items;
g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
/* If a section is already shown, it is handled in search change callback */
if (self->visible_section)
return;
model = G_LIST_MODEL (self->filtered_shortcuts);
n_items = g_list_model_get_n_items (model);
if (n_items == 0)
page = GTK_WIDGET (self->empty_results_page);
else if (self->search_terms)
page = GTK_WIDGET (self->search_result_page);
else
page = GTK_WIDGET (self->section_list_page);
gtk_stack_set_visible_child (self->section_stack, page);
}
/* All items have loaded, now sort the groups and add them to the page */
static void
shortcuts_loaded_cb (CcKeyboardShortcutDialog *self)
{
g_autoptr(GPtrArray) filtered_items = NULL;
g_autoptr(GPtrArray) widgets = NULL;
GListStore *filtered_lists;
GListStore *custom_store;
guint n_items;
/* Ensure that custom shorcuts section exists */
custom_store = keyboard_shortcut_get_section_store (self, "custom", "Custom Shortcuts");
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
widgets = g_ptr_array_new ();
filtered_items = g_ptr_array_new ();
filtered_lists = g_list_store_new (G_TYPE_LIST_MODEL);
g_signal_connect_object (custom_store, "items-changed",
G_CALLBACK (shortuct_custom_items_changed),
self, G_CONNECT_SWAPPED);
g_list_store_sort (self->sections, compare_sections_title, NULL);
for (guint i = 0; i < n_items; i++)
{
g_autoptr(GObject) item = NULL;
CcKeyboardShortcutGroup *group;
GListModel *model;
GtkWidget *page;
item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
group = g_object_get_data (item, "search-group");
g_ptr_array_add (widgets, group);
model = cc_keyboard_shortcut_group_get_model (group);
g_ptr_array_add (filtered_items, model);
/* Populate shortcut section page */
group = g_object_get_data (item, "group");
page = adw_preferences_page_new ();
g_object_set_data (item, "page", page);
adw_preferences_page_add (ADW_PREFERENCES_PAGE (page), ADW_PREFERENCES_GROUP (group));
gtk_stack_add_child (self->shortcut_list_stack, page);
}
/* Populate search results page */
for (guint i = 0; i < widgets->len; i++)
adw_preferences_page_add (self->search_result_page, widgets->pdata[i]);
/* Keep track of search results so as to update empty state */
g_list_store_splice (filtered_lists, 0, 0, filtered_items->pdata, filtered_items->len);
self->filtered_shortcuts = gtk_flatten_list_model_new (G_LIST_MODEL (filtered_lists));
g_signal_connect_object (self->filtered_shortcuts, "items-changed",
G_CALLBACK (shortcut_search_result_changed_cb),
self, G_CONNECT_SWAPPED);
}
static GtkWidget *
shortcut_dialog_row_new (gpointer item,
gpointer user_data)
{
GtkWidget *row, *group;
const char *title;
group = g_object_get_data (item, "search-group");
title = g_object_get_data (item, "title");
row = g_object_new (CC_TYPE_LIST_ROW, NULL);
g_object_set_data (G_OBJECT (row), "section", item);
cc_list_row_set_show_arrow (CC_LIST_ROW (row), TRUE);
gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE);
adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), _(title));
g_object_bind_property (group, "modified-text",
row, "secondary-label",
G_BINDING_SYNC_CREATE);
return row;
}
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_set_visible (self->shortcut_editor, TRUE);
}
static void
on_reset_all_dialog_response_cb (CcKeyboardShortcutDialog *self,
gchar *response,
AdwMessageDialog *dialog)
{
guint n_items, j_items;
gtk_window_destroy (GTK_WINDOW (dialog));
if (g_strcmp0 (response, "cancel") == 0)
return;
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
for (guint i = 0; i < n_items; i++)
{
g_autoptr(GListModel) section = NULL;
section = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
j_items = g_list_model_get_n_items (section);
for (guint j = 0; j < j_items; j++)
{
g_autoptr(CcKeyboardItem) item = NULL;
item = g_list_model_get_item (section, j);
/* Don't reset custom shortcuts */
if (cc_keyboard_item_get_item_type (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, item);
}
}
}
static void
reset_all_clicked_cb (CcKeyboardShortcutDialog *self)
{
GtkWidget *dialog;
dialog = adw_message_dialog_new (GTK_WINDOW (self),
_("Reset All Shortcuts?"),
NULL);
adw_message_dialog_format_body (ADW_MESSAGE_DIALOG (dialog),
_("Resetting the shortcuts may affect your custom shortcuts. This cannot be undone."));
adw_message_dialog_add_responses (ADW_MESSAGE_DIALOG (dialog),
"cancel", _("Cancel"),
"reset_all", _("Reset All"),
NULL);
adw_message_dialog_set_response_appearance (ADW_MESSAGE_DIALOG (dialog),
"reset_all",
ADW_RESPONSE_DESTRUCTIVE);
adw_message_dialog_set_default_response (ADW_MESSAGE_DIALOG (dialog),
"cancel");
adw_message_dialog_set_close_response (ADW_MESSAGE_DIALOG (dialog),
"cancel");
g_signal_connect_swapped (dialog,
"response",
G_CALLBACK (on_reset_all_dialog_response_cb),
self);
gtk_window_present (GTK_WINDOW (dialog));
}
static void
shortcut_dialog_visible_page_changed_cb (CcKeyboardShortcutDialog *self)
{
gpointer visible_page;
gboolean is_main_view;
visible_page = adw_navigation_view_get_visible_page (self->navigation_view);
is_main_view = visible_page == self->main_page;
if (is_main_view)
{
gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
self->visible_section = NULL;
}
else if (self->visible_section)
{
const char *title;
title = g_object_get_data (G_OBJECT (self->visible_section), "title");
adw_navigation_page_set_title (self->subview_page, _(title) ?: "");
}
}
static void
shortcut_search_entry_changed_cb (CcKeyboardShortcutDialog *self)
{
g_autofree char *search = NULL;
const char *search_text;
guint n_items;
g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
/* Don't update search if we are in a subview */
if (self->visible_section)
return;
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->sections));
search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
search = cc_util_normalize_casefold_and_unaccent (search_text);
g_clear_pointer (&self->search_terms, g_strfreev);
if (search && *search && *search != ' ')
self->search_terms = g_strsplit (search, " ", -1);
/* "Reset all..." button should be sensitive only if the search is not active */
gtk_widget_set_sensitive (GTK_WIDGET (self->reset_all_button), !self->search_terms);
for (guint i = 0; i < n_items; i++)
{
g_autoptr(GObject) item = NULL;
CcKeyboardShortcutGroup *group;
item = g_list_model_get_item (G_LIST_MODEL (self->sections), i);
group = g_object_get_data (item, "search-group");
cc_keyboard_shortcut_group_set_filter (group, self->search_terms);
}
shortcut_search_result_changed_cb (self);
}
static void
shortcut_search_entry_stopped_cb (CcKeyboardShortcutDialog *self)
{
const char *search_text;
search_text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
if (search_text && g_strcmp0 (search_text, "") != 0)
gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
else
gtk_window_close (GTK_WINDOW (self));
}
static void
shortcut_section_row_activated_cb (CcKeyboardShortcutDialog *self,
GtkListBoxRow *row)
{
GListStore *section;
GtkWidget *page;
g_assert (CC_IS_KEYBOARD_SHORTCUT_DIALOG (self));
g_assert (GTK_IS_LIST_BOX_ROW (row));
section = g_object_get_data (G_OBJECT (row), "section");
self->visible_section = section;
page = g_object_get_data (G_OBJECT (section), "page");
gtk_stack_set_visible_child (self->shortcut_list_stack, page);
adw_navigation_view_push (self->navigation_view, self->subview_page);
shortuct_custom_items_changed (self);
}
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_object (&self->sections);
g_clear_pointer (&self->search_terms, g_strfreev);
g_clear_object (&self->sections);
g_clear_object (&self->filtered_shortcuts);
g_clear_pointer ((GtkWindow**)&self->shortcut_editor, gtk_window_destroy);
G_OBJECT_CLASS (cc_keyboard_shortcut_dialog_parent_class)->finalize (object);
}
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_add_binding_action (widget_class, GDK_KEY_Escape, 0, "window.close", NULL);
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, navigation_view);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, main_page);
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_stack);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_list_page);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, section_list_box);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, search_result_page);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_results_page);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_page);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, subview_stack);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, shortcut_list_stack);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, empty_custom_shortcut_page);
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutDialog, accelerator_size_group);
gtk_widget_class_bind_template_callback (widget_class, add_custom_shortcut_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, reset_all_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, shortcut_dialog_visible_page_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, shortcut_search_entry_changed_cb);
gtk_widget_class_bind_template_callback (widget_class, shortcut_search_entry_stopped_cb);
gtk_widget_class_bind_template_callback (widget_class, shortcut_section_row_activated_cb);
}
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));
shortcut_dialog_visible_page_changed_cb (self);
self->manager = cc_keyboard_manager_new ();
self->shortcut_editor = cc_keyboard_shortcut_editor_new (self->manager);
shortcut_dialog_visible_page_changed_cb (self);
self->sections = g_list_store_new (G_TYPE_LIST_STORE);
g_signal_connect_object (self->manager,
"shortcut-added",
G_CALLBACK (shortcut_added_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->manager,
"shortcut-removed",
G_CALLBACK (shortcut_removed_cb),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->manager,
"shortcuts-loaded",
G_CALLBACK (shortcuts_loaded_cb),
self, G_CONNECT_SWAPPED);
cc_keyboard_manager_load_shortcuts (self->manager);
gtk_list_box_bind_model (self->section_list_box,
G_LIST_MODEL (self->sections),
shortcut_dialog_row_new,
self, NULL);
}
GtkWidget*
cc_keyboard_shortcut_dialog_new (void)
{
return g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_DIALOG, NULL);
}