The way shortcuts in mutter/gnome-shell work is that it looks up the keycode that generates the shortcut keyval at the lowest shift level and then checks if all the modifiers match. This does not work for shortcuts that for example include "dollar" to represent "<Shift>4", because on some keyboards/layout there is a separate dollar key key with its own keycode. This would be at a lower shift level than "<Shift>4". By always translating such shortcuts to "<Shift>number", we make sure the resulting shortcut will work in the shell and is closer to what the user likely intended the shortcut to be, because numbers are usually assigned to things that can be enumerated, such as workspaces or favorite applications. This also special cases the num-row key on layouts such as AZERTY, where the number is the shifted keyval, to always prefer the number. Due to the way the shell interprets these shortcuts, they still work and by always using numbers they work across different layouts. This change also fixes that pressing "<Shift><Super>4" was turned into "<Shift><Super>dollar", which effectively included the "<Shift>" twice. Fixes: https://gitlab.gnome.org/GNOME/gnome-control-center/-/issues/1528
985 lines
32 KiB
C
985 lines
32 KiB
C
/* cc-keyboard-shortcut-editor.h
|
|
*
|
|
* Copyright (C) 2016 Endless, Inc
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Authors: Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
|
|
*/
|
|
|
|
#include <glib-object.h>
|
|
#include <glib/gi18n.h>
|
|
|
|
#include "cc-keyboard-shortcut-editor.h"
|
|
#include "keyboard-shortcuts.h"
|
|
|
|
struct _CcKeyboardShortcutEditor
|
|
{
|
|
GtkDialog parent;
|
|
|
|
GtkButton *add_button;
|
|
GtkButton *cancel_button;
|
|
GtkButton *change_custom_shortcut_button;
|
|
GtkEntry *command_entry;
|
|
GtkGrid *custom_grid;
|
|
GtkShortcutLabel *custom_shortcut_accel_label;
|
|
GtkStack *custom_shortcut_stack;
|
|
GtkBox *edit_box;
|
|
GtkHeaderBar *headerbar;
|
|
GtkEntry *name_entry;
|
|
GtkLabel *new_shortcut_conflict_label;
|
|
GtkButton *remove_button;
|
|
GtkButton *replace_button;
|
|
GtkButton *reset_button;
|
|
GtkButton *reset_custom_button;
|
|
GtkButton *set_button;
|
|
GtkShortcutLabel *shortcut_accel_label;
|
|
GtkLabel *shortcut_conflict_label;
|
|
GtkBox *standard_box;
|
|
GtkStack *stack;
|
|
GtkLabel *top_info_label;
|
|
|
|
CcShortcutEditorMode mode;
|
|
|
|
CcKeyboardManager *manager;
|
|
CcKeyboardItem *item;
|
|
GBinding *reset_item_binding;
|
|
|
|
CcKeyboardItem *collision_item;
|
|
|
|
/* Custom shortcuts */
|
|
gboolean system_shortcuts_inhibited;
|
|
guint grab_idle_id;
|
|
|
|
CcKeyCombo *custom_combo;
|
|
gboolean custom_is_modifier;
|
|
gboolean edited : 1;
|
|
};
|
|
|
|
static void command_entry_changed_cb (CcKeyboardShortcutEditor *self);
|
|
static void name_entry_changed_cb (CcKeyboardShortcutEditor *self);
|
|
static void set_button_clicked_cb (CcKeyboardShortcutEditor *self);
|
|
|
|
G_DEFINE_TYPE (CcKeyboardShortcutEditor, cc_keyboard_shortcut_editor, GTK_TYPE_DIALOG)
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_KEYBOARD_ITEM,
|
|
PROP_MANAGER,
|
|
N_PROPS
|
|
};
|
|
|
|
typedef enum
|
|
{
|
|
HEADER_MODE_NONE,
|
|
HEADER_MODE_ADD,
|
|
HEADER_MODE_SET,
|
|
HEADER_MODE_REPLACE,
|
|
HEADER_MODE_CUSTOM_CANCEL,
|
|
HEADER_MODE_CUSTOM_EDIT
|
|
} HeaderMode;
|
|
|
|
typedef enum
|
|
{
|
|
PAGE_CUSTOM,
|
|
PAGE_EDIT,
|
|
PAGE_STANDARD,
|
|
} ShortcutEditorPage;
|
|
|
|
static GParamSpec *properties [N_PROPS] = { NULL, };
|
|
|
|
/* Getter and setter for ShortcutEditorPage */
|
|
static ShortcutEditorPage
|
|
get_shortcut_editor_page (CcKeyboardShortcutEditor *self)
|
|
{
|
|
if (gtk_stack_get_visible_child (self->stack) == GTK_WIDGET (self->edit_box))
|
|
return PAGE_EDIT;
|
|
|
|
if (gtk_stack_get_visible_child (self->stack) == GTK_WIDGET (self->custom_grid))
|
|
return PAGE_CUSTOM;
|
|
|
|
return PAGE_STANDARD;
|
|
}
|
|
|
|
static void
|
|
set_shortcut_editor_page (CcKeyboardShortcutEditor *self,
|
|
ShortcutEditorPage page)
|
|
{
|
|
switch (page)
|
|
{
|
|
case PAGE_CUSTOM:
|
|
gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->custom_grid));
|
|
break;
|
|
|
|
case PAGE_EDIT:
|
|
gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->edit_box));
|
|
break;
|
|
|
|
case PAGE_STANDARD:
|
|
gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->standard_box));
|
|
break;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
gtk_widget_set_visible (GTK_WIDGET (self->top_info_label), page != PAGE_CUSTOM);
|
|
}
|
|
|
|
static void
|
|
apply_custom_item_fields (CcKeyboardShortcutEditor *self,
|
|
CcKeyboardItem *item)
|
|
{
|
|
/* Only setup the binding when it was actually edited */
|
|
if (self->edited)
|
|
{
|
|
CcKeyCombo *combo = self->custom_combo;
|
|
|
|
cc_keyboard_item_disable (item);
|
|
|
|
if (combo->keycode != 0 || combo->keyval != 0 || combo->mask != 0)
|
|
cc_keyboard_item_add_key_combo (item, combo);
|
|
}
|
|
|
|
/* Set the keyboard shortcut name and command for custom entries */
|
|
if (cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
|
|
{
|
|
g_settings_set_string (cc_keyboard_item_get_settings (item),
|
|
"name",
|
|
gtk_editable_get_text (GTK_EDITABLE (self->name_entry)));
|
|
g_settings_set_string (cc_keyboard_item_get_settings (item),
|
|
"command",
|
|
gtk_editable_get_text (GTK_EDITABLE (self->command_entry)));
|
|
}
|
|
}
|
|
|
|
static void
|
|
clear_custom_entries (CcKeyboardShortcutEditor *self)
|
|
{
|
|
g_signal_handlers_block_by_func (self->command_entry, command_entry_changed_cb, self);
|
|
g_signal_handlers_block_by_func (self->name_entry, name_entry_changed_cb, self);
|
|
|
|
gtk_editable_set_text (GTK_EDITABLE (self->name_entry), "");
|
|
gtk_editable_set_text (GTK_EDITABLE (self->command_entry), "");
|
|
|
|
gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->custom_shortcut_accel_label), "");
|
|
gtk_label_set_label (self->new_shortcut_conflict_label, "");
|
|
gtk_label_set_label (self->shortcut_conflict_label, "");
|
|
|
|
memset (self->custom_combo, 0, sizeof (CcKeyCombo));
|
|
self->custom_is_modifier = TRUE;
|
|
self->edited = FALSE;
|
|
|
|
self->collision_item = NULL;
|
|
|
|
g_signal_handlers_unblock_by_func (self->command_entry, command_entry_changed_cb, self);
|
|
g_signal_handlers_unblock_by_func (self->name_entry, name_entry_changed_cb, self);
|
|
}
|
|
|
|
static void
|
|
cancel_editing (CcKeyboardShortcutEditor *self)
|
|
{
|
|
cc_keyboard_shortcut_editor_set_item (self, NULL);
|
|
clear_custom_entries (self);
|
|
|
|
gtk_widget_hide (GTK_WIDGET (self));
|
|
}
|
|
|
|
static gboolean
|
|
is_custom_shortcut (CcKeyboardShortcutEditor *self) {
|
|
return self->item == NULL || cc_keyboard_item_get_item_type (self->item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH;
|
|
}
|
|
|
|
static void
|
|
inhibit_system_shortcuts (CcKeyboardShortcutEditor *self)
|
|
{
|
|
GtkNative *native;
|
|
GdkSurface *surface;
|
|
|
|
if (self->system_shortcuts_inhibited)
|
|
return;
|
|
|
|
native = gtk_widget_get_native (GTK_WIDGET (self));
|
|
surface = gtk_native_get_surface (native);
|
|
|
|
if (GDK_IS_TOPLEVEL (surface))
|
|
{
|
|
gdk_toplevel_inhibit_system_shortcuts (GDK_TOPLEVEL (surface), NULL);
|
|
self->system_shortcuts_inhibited = TRUE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
uninhibit_system_shortcuts (CcKeyboardShortcutEditor *self)
|
|
{
|
|
GtkNative *native;
|
|
GdkSurface *surface;
|
|
|
|
if (!self->system_shortcuts_inhibited)
|
|
return;
|
|
|
|
native = gtk_widget_get_native (GTK_WIDGET (self));
|
|
surface = gtk_native_get_surface (native);
|
|
|
|
if (GDK_IS_TOPLEVEL (surface))
|
|
{
|
|
gdk_toplevel_restore_system_shortcuts (GDK_TOPLEVEL (surface));
|
|
self->system_shortcuts_inhibited = FALSE;
|
|
}
|
|
}
|
|
|
|
static void
|
|
update_shortcut (CcKeyboardShortcutEditor *self)
|
|
{
|
|
if (!self->item)
|
|
return;
|
|
|
|
/* Setup the binding */
|
|
apply_custom_item_fields (self, self->item);
|
|
|
|
/* Eventually disable the conflict shortcut */
|
|
if (self->collision_item)
|
|
cc_keyboard_item_disable (self->collision_item);
|
|
|
|
/* Cleanup whatever was set before */
|
|
clear_custom_entries (self);
|
|
|
|
cc_keyboard_shortcut_editor_set_item (self, NULL);
|
|
}
|
|
|
|
static GtkShortcutLabel*
|
|
get_current_shortcut_label (CcKeyboardShortcutEditor *self)
|
|
{
|
|
if (is_custom_shortcut (self))
|
|
return GTK_SHORTCUT_LABEL (self->custom_shortcut_accel_label);
|
|
|
|
return GTK_SHORTCUT_LABEL (self->shortcut_accel_label);
|
|
}
|
|
|
|
static void
|
|
set_header_mode (CcKeyboardShortcutEditor *self,
|
|
HeaderMode mode)
|
|
{
|
|
gtk_header_bar_set_show_title_buttons (self->headerbar, mode == HEADER_MODE_CUSTOM_EDIT);
|
|
|
|
gtk_widget_set_visible (GTK_WIDGET (self->add_button), mode == HEADER_MODE_ADD);
|
|
gtk_widget_set_visible (GTK_WIDGET (self->cancel_button), mode != HEADER_MODE_NONE &&
|
|
mode != HEADER_MODE_CUSTOM_EDIT);
|
|
gtk_widget_set_visible (GTK_WIDGET (self->replace_button), mode == HEADER_MODE_REPLACE);
|
|
gtk_widget_set_visible (GTK_WIDGET (self->set_button), mode == HEADER_MODE_SET);
|
|
gtk_widget_set_visible (GTK_WIDGET (self->remove_button), mode == HEADER_MODE_CUSTOM_EDIT);
|
|
|
|
/* By setting the default response, the action button gets the 'suggested-action' applied */
|
|
switch (mode)
|
|
{
|
|
case HEADER_MODE_SET:
|
|
gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_APPLY);
|
|
break;
|
|
|
|
case HEADER_MODE_REPLACE:
|
|
gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT);
|
|
break;
|
|
|
|
case HEADER_MODE_ADD:
|
|
gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_OK);
|
|
break;
|
|
|
|
default:
|
|
gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_NONE);
|
|
}
|
|
}
|
|
|
|
static void
|
|
setup_custom_shortcut (CcKeyboardShortcutEditor *self)
|
|
{
|
|
GtkShortcutLabel *shortcut_label;
|
|
CcKeyboardItem *collision_item;
|
|
HeaderMode mode;
|
|
gboolean is_custom, is_accel_empty;
|
|
gboolean valid, accel_valid;
|
|
g_autofree char *accel = NULL;
|
|
|
|
is_custom = is_custom_shortcut (self);
|
|
accel_valid = is_valid_binding (self->custom_combo) &&
|
|
is_valid_accel (self->custom_combo) &&
|
|
!self->custom_is_modifier;
|
|
|
|
is_accel_empty = is_empty_binding (self->custom_combo);
|
|
|
|
if (is_accel_empty)
|
|
accel_valid = TRUE;
|
|
valid = accel_valid;
|
|
|
|
/* Additional checks for custom shortcuts */
|
|
if (is_custom)
|
|
{
|
|
if (accel_valid)
|
|
{
|
|
set_shortcut_editor_page (self, PAGE_CUSTOM);
|
|
|
|
/* We have to check if the current accelerator is empty in order to
|
|
* decide if we show the "Set Shortcut" button or the accelerator label */
|
|
gtk_stack_set_visible_child (self->custom_shortcut_stack,
|
|
is_accel_empty ? GTK_WIDGET (self->change_custom_shortcut_button) : GTK_WIDGET (self->custom_shortcut_accel_label));
|
|
gtk_widget_set_visible (GTK_WIDGET (self->reset_custom_button), !is_accel_empty);
|
|
}
|
|
|
|
valid = accel_valid &&
|
|
gtk_entry_get_text_length (self->name_entry) > 0 &&
|
|
gtk_entry_get_text_length (self->command_entry) > 0;
|
|
}
|
|
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->replace_button), valid);
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), valid);
|
|
if (valid)
|
|
set_header_mode (self, HEADER_MODE_ADD);
|
|
else
|
|
set_header_mode (self, is_custom ? HEADER_MODE_CUSTOM_CANCEL : HEADER_MODE_NONE);
|
|
|
|
/* Nothing else to do if the shortcut is invalid */
|
|
if (!accel_valid)
|
|
return;
|
|
|
|
/* Valid shortcut, show it in the standard page */
|
|
if (!is_custom)
|
|
set_shortcut_editor_page (self, PAGE_STANDARD);
|
|
|
|
shortcut_label = get_current_shortcut_label (self);
|
|
|
|
collision_item = cc_keyboard_manager_get_collision (self->manager,
|
|
self->item,
|
|
self->custom_combo);
|
|
|
|
accel = gtk_accelerator_name (self->custom_combo->keyval, self->custom_combo->mask);
|
|
|
|
|
|
/* Setup the accelerator label */
|
|
gtk_shortcut_label_set_accelerator (shortcut_label, accel);
|
|
|
|
self->edited = TRUE;
|
|
|
|
uninhibit_system_shortcuts (self);
|
|
|
|
/*
|
|
* Oops! Looks like the accelerator is already being used, so we
|
|
* must warn the user and let it be very clear that adding this
|
|
* shortcut will disable the other.
|
|
*/
|
|
gtk_widget_set_visible (GTK_WIDGET (self->new_shortcut_conflict_label), collision_item != NULL);
|
|
|
|
if (collision_item)
|
|
{
|
|
GtkLabel *label;
|
|
g_autofree gchar *friendly_accelerator = NULL;
|
|
g_autofree gchar *accelerator_text = NULL;
|
|
g_autofree gchar *collision_text = NULL;
|
|
|
|
friendly_accelerator = convert_keysym_state_to_string (self->custom_combo);
|
|
|
|
accelerator_text = g_strdup_printf ("<b>%s</b>", friendly_accelerator);
|
|
collision_text = g_strdup_printf (_("%s is already being used for %s. If you "
|
|
"replace it, %s will be disabled"),
|
|
accelerator_text,
|
|
cc_keyboard_item_get_description (collision_item),
|
|
cc_keyboard_item_get_description (collision_item));
|
|
|
|
label = is_custom_shortcut (self) ? self->new_shortcut_conflict_label : self->shortcut_conflict_label;
|
|
|
|
gtk_label_set_markup (label, collision_text);
|
|
}
|
|
|
|
/*
|
|
* When there is a collision between the current shortcut and another shortcut,
|
|
* and we're editing an existing shortcut (rather than creating a new one), setup
|
|
* the headerbar to display "Cancel" and "Replace". Otherwise, make sure to set
|
|
* only the close button again.
|
|
*/
|
|
if (collision_item)
|
|
{
|
|
mode = HEADER_MODE_REPLACE;
|
|
}
|
|
else
|
|
{
|
|
if (self->mode == CC_SHORTCUT_EDITOR_EDIT)
|
|
mode = is_custom ? HEADER_MODE_CUSTOM_EDIT : HEADER_MODE_SET;
|
|
else
|
|
mode = is_custom ? HEADER_MODE_ADD : HEADER_MODE_SET;
|
|
}
|
|
|
|
set_header_mode (self, mode);
|
|
|
|
self->collision_item = collision_item;
|
|
}
|
|
|
|
static void
|
|
add_button_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
CcKeyboardItem *item;
|
|
|
|
item = cc_keyboard_manager_create_custom_shortcut (self->manager);
|
|
|
|
/* Apply the custom shortcut setup at the new item */
|
|
apply_custom_item_fields (self, item);
|
|
|
|
/* Eventually disable the conflict shortcut */
|
|
if (self->collision_item)
|
|
cc_keyboard_item_disable (self->collision_item);
|
|
|
|
/* Cleanup everything once we're done */
|
|
clear_custom_entries (self);
|
|
|
|
cc_keyboard_manager_add_custom_shortcut (self->manager, item);
|
|
|
|
gtk_widget_hide (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
cancel_button_clicked_cb (GtkWidget *button,
|
|
CcKeyboardShortcutEditor *self)
|
|
{
|
|
cancel_editing (self);
|
|
}
|
|
|
|
static void
|
|
change_custom_shortcut_button_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
inhibit_system_shortcuts (self);
|
|
set_shortcut_editor_page (self, PAGE_EDIT);
|
|
set_header_mode (self, HEADER_MODE_NONE);
|
|
}
|
|
|
|
static void
|
|
command_entry_changed_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
setup_custom_shortcut (self);
|
|
}
|
|
|
|
static void
|
|
name_entry_changed_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
setup_custom_shortcut (self);
|
|
}
|
|
|
|
static void
|
|
remove_button_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
gtk_widget_hide (GTK_WIDGET (self));
|
|
|
|
cc_keyboard_manager_remove_custom_shortcut (self->manager, self->item);
|
|
}
|
|
|
|
static void
|
|
replace_button_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
if (self->mode == CC_SHORTCUT_EDITOR_CREATE)
|
|
add_button_clicked_cb (self);
|
|
else
|
|
set_button_clicked_cb (self);
|
|
}
|
|
|
|
static void
|
|
reset_custom_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
if (self->item)
|
|
cc_keyboard_manager_reset_shortcut (self->manager, self->item);
|
|
|
|
gtk_stack_set_visible_child (self->custom_shortcut_stack, GTK_WIDGET (self->change_custom_shortcut_button));
|
|
gtk_widget_hide (GTK_WIDGET (self->reset_custom_button));
|
|
}
|
|
|
|
static void
|
|
reset_item_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
CcKeyCombo combo;
|
|
gchar *accel;
|
|
|
|
/* Reset first, then update the shortcut */
|
|
cc_keyboard_manager_reset_shortcut (self->manager, self->item);
|
|
|
|
combo = cc_keyboard_item_get_primary_combo (self->item);
|
|
accel = gtk_accelerator_name (combo.keyval, combo.mask);
|
|
gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->shortcut_accel_label), accel);
|
|
|
|
g_free (accel);
|
|
}
|
|
|
|
static void
|
|
set_button_clicked_cb (CcKeyboardShortcutEditor *self)
|
|
{
|
|
update_shortcut (self);
|
|
gtk_widget_hide (GTK_WIDGET (self));
|
|
}
|
|
|
|
static void
|
|
setup_keyboard_item (CcKeyboardShortcutEditor *self,
|
|
CcKeyboardItem *item)
|
|
{
|
|
CcKeyCombo combo;
|
|
gboolean is_custom;
|
|
g_autofree gchar *accel = NULL;
|
|
g_autofree gchar *description_text = NULL;
|
|
g_autofree gchar *text = NULL;
|
|
|
|
if (!item) {
|
|
gtk_label_set_text (self->top_info_label, _("Enter the new shortcut"));
|
|
return;
|
|
}
|
|
|
|
combo = cc_keyboard_item_get_primary_combo (item);
|
|
is_custom = cc_keyboard_item_get_item_type (item) == CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH;
|
|
accel = gtk_accelerator_name (combo.keyval, combo.mask);
|
|
|
|
/* To avoid accidentally thinking we unset the current keybinding, set the values
|
|
* of the keyboard item that is being edited */
|
|
self->custom_is_modifier = FALSE;
|
|
*self->custom_combo = combo;
|
|
|
|
/* Headerbar */
|
|
gtk_window_set_title (GTK_WINDOW (self),
|
|
is_custom ? _("Set Custom Shortcut") : _("Set Shortcut"));
|
|
|
|
set_header_mode (self, is_custom ? HEADER_MODE_CUSTOM_EDIT : HEADER_MODE_NONE);
|
|
|
|
gtk_widget_hide (GTK_WIDGET (self->add_button));
|
|
gtk_widget_hide (GTK_WIDGET (self->cancel_button));
|
|
gtk_widget_hide (GTK_WIDGET (self->replace_button));
|
|
|
|
/* Setup the top label */
|
|
description_text = g_strdup_printf ("<b>%s</b>", cc_keyboard_item_get_description (item));
|
|
/* TRANSLATORS: %s is replaced with a description of the keyboard shortcut */
|
|
text = g_strdup_printf (_("Enter new shortcut to change %s."), description_text);
|
|
|
|
gtk_label_set_markup (self->top_info_label, text);
|
|
|
|
/* Accelerator labels */
|
|
gtk_shortcut_label_set_accelerator (self->shortcut_accel_label, accel);
|
|
gtk_shortcut_label_set_accelerator (self->custom_shortcut_accel_label, accel);
|
|
|
|
g_clear_pointer (&self->reset_item_binding, g_binding_unbind);
|
|
self->reset_item_binding = g_object_bind_property (item,
|
|
"is-value-default",
|
|
self->reset_button,
|
|
"visible",
|
|
G_BINDING_DEFAULT | G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
|
|
|
|
/* Setup the custom entries */
|
|
if (is_custom)
|
|
{
|
|
gboolean is_accel_empty;
|
|
|
|
g_signal_handlers_block_by_func (self->command_entry, command_entry_changed_cb, self);
|
|
g_signal_handlers_block_by_func (self->name_entry, name_entry_changed_cb, self);
|
|
|
|
/* Name entry */
|
|
gtk_editable_set_text (GTK_EDITABLE (self->name_entry), cc_keyboard_item_get_description (item));
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->name_entry), cc_keyboard_item_get_desc_editable (item));
|
|
|
|
/* Command entry */
|
|
gtk_editable_set_text (GTK_EDITABLE (self->command_entry), cc_keyboard_item_get_command (item));
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->command_entry), cc_keyboard_item_get_cmd_editable (item));
|
|
|
|
/* If there is no accelerator set for this custom shortcut, show the "Set Shortcut" button. */
|
|
is_accel_empty = !accel || accel[0] == '\0';
|
|
|
|
gtk_stack_set_visible_child (self->custom_shortcut_stack,
|
|
is_accel_empty ? GTK_WIDGET (self->change_custom_shortcut_button) : GTK_WIDGET (self->custom_shortcut_accel_label));
|
|
|
|
gtk_widget_set_visible (GTK_WIDGET (self->reset_custom_button), !is_accel_empty);
|
|
|
|
g_signal_handlers_unblock_by_func (self->command_entry, command_entry_changed_cb, self);
|
|
g_signal_handlers_unblock_by_func (self->name_entry, name_entry_changed_cb, self);
|
|
|
|
uninhibit_system_shortcuts (self);
|
|
}
|
|
|
|
/* Show the appropriate view */
|
|
set_shortcut_editor_page (self, is_custom ? PAGE_CUSTOM : PAGE_EDIT);
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_finalize (GObject *object)
|
|
{
|
|
CcKeyboardShortcutEditor *self = (CcKeyboardShortcutEditor *)object;
|
|
|
|
g_clear_object (&self->item);
|
|
g_clear_object (&self->manager);
|
|
|
|
g_clear_pointer (&self->custom_combo, g_free);
|
|
|
|
G_OBJECT_CLASS (cc_keyboard_shortcut_editor_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_KEYBOARD_ITEM:
|
|
g_value_set_object (value, self->item);
|
|
break;
|
|
|
|
case PROP_MANAGER:
|
|
g_value_set_object (value, self->manager);
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_KEYBOARD_ITEM:
|
|
cc_keyboard_shortcut_editor_set_item (self, g_value_get_object (value));
|
|
break;
|
|
|
|
case PROP_MANAGER:
|
|
g_set_object (&self->manager, g_value_get_object (value));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
on_key_pressed_cb (GtkEventControllerKey *key_controller,
|
|
guint keyval,
|
|
guint keycode,
|
|
GdkModifierType state,
|
|
CcKeyboardShortcutEditor *self)
|
|
{
|
|
GdkModifierType real_mask;
|
|
GdkEvent *event;
|
|
gboolean editing;
|
|
gboolean is_modifier;
|
|
guint keyval_lower;
|
|
|
|
/* Being in the "change-shortcut" page is the only check we must
|
|
* perform to decide if we're editing a shortcut. */
|
|
editing = get_shortcut_editor_page (self) == PAGE_EDIT;
|
|
|
|
if (!editing)
|
|
return GDK_EVENT_PROPAGATE;
|
|
|
|
normalize_keyval_and_mask (keycode, state,
|
|
gtk_event_controller_key_get_group (key_controller),
|
|
&keyval_lower, &real_mask);
|
|
|
|
event = gtk_event_controller_get_current_event (GTK_EVENT_CONTROLLER (key_controller));
|
|
is_modifier = gdk_key_event_is_modifier (event);
|
|
|
|
/* A single Escape press cancels the editing */
|
|
if (!is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_Escape)
|
|
{
|
|
self->edited = FALSE;
|
|
|
|
uninhibit_system_shortcuts (self);
|
|
cancel_editing (self);
|
|
|
|
return GDK_EVENT_STOP;
|
|
}
|
|
|
|
/* Backspace disables the current shortcut */
|
|
if (!is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_BackSpace)
|
|
{
|
|
self->edited = TRUE;
|
|
self->custom_is_modifier = FALSE;
|
|
memset (self->custom_combo, 0, sizeof (CcKeyCombo));
|
|
|
|
gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->custom_shortcut_accel_label), "");
|
|
gtk_shortcut_label_set_accelerator (GTK_SHORTCUT_LABEL (self->shortcut_accel_label), "");
|
|
|
|
uninhibit_system_shortcuts (self);
|
|
|
|
self->edited = FALSE;
|
|
|
|
setup_custom_shortcut (self);
|
|
|
|
return GDK_EVENT_STOP;
|
|
}
|
|
|
|
self->custom_is_modifier = is_modifier;
|
|
self->custom_combo->keycode = keycode;
|
|
self->custom_combo->keyval = keyval_lower;
|
|
self->custom_combo->mask = real_mask;
|
|
|
|
/* CapsLock isn't supported as a keybinding modifier, so keep it from confusing us */
|
|
self->custom_combo->mask &= ~GDK_LOCK_MASK;
|
|
|
|
setup_custom_shortcut (self);
|
|
|
|
return GDK_EVENT_STOP;
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_close (GtkDialog *dialog)
|
|
{
|
|
CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (dialog);
|
|
|
|
if (self->mode == CC_SHORTCUT_EDITOR_EDIT)
|
|
update_shortcut (self);
|
|
|
|
GTK_DIALOG_CLASS (cc_keyboard_shortcut_editor_parent_class)->close (dialog);
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_response (GtkDialog *dialog,
|
|
gint response_id)
|
|
{
|
|
CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (dialog);
|
|
|
|
if (response_id == GTK_RESPONSE_DELETE_EVENT &&
|
|
self->mode == CC_SHORTCUT_EDITOR_EDIT)
|
|
{
|
|
update_shortcut (self);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
grab_idle (gpointer data)
|
|
{
|
|
CcKeyboardShortcutEditor *self = data;
|
|
|
|
if (self->item && cc_keyboard_item_get_item_type (self->item) != CC_KEYBOARD_ITEM_TYPE_GSETTINGS_PATH)
|
|
inhibit_system_shortcuts (self);
|
|
|
|
self->grab_idle_id = 0;
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_show (GtkWidget *widget)
|
|
{
|
|
CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (widget);
|
|
|
|
/* Map before grabbing, so that the window is visible */
|
|
GTK_WIDGET_CLASS (cc_keyboard_shortcut_editor_parent_class)->show (widget);
|
|
|
|
self->grab_idle_id = g_timeout_add (100, grab_idle, self);
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_unrealize (GtkWidget *widget)
|
|
{
|
|
CcKeyboardShortcutEditor *self = CC_KEYBOARD_SHORTCUT_EDITOR (widget);
|
|
|
|
if (self->grab_idle_id) {
|
|
g_source_remove (self->grab_idle_id);
|
|
self->grab_idle_id = 0;
|
|
}
|
|
|
|
uninhibit_system_shortcuts (self);
|
|
|
|
GTK_WIDGET_CLASS (cc_keyboard_shortcut_editor_parent_class)->unrealize (widget);
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_class_init (CcKeyboardShortcutEditorClass *klass)
|
|
{
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
GtkDialogClass *dialog_class = GTK_DIALOG_CLASS (klass);
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->finalize = cc_keyboard_shortcut_editor_finalize;
|
|
object_class->get_property = cc_keyboard_shortcut_editor_get_property;
|
|
object_class->set_property = cc_keyboard_shortcut_editor_set_property;
|
|
|
|
widget_class->show = cc_keyboard_shortcut_editor_show;
|
|
widget_class->unrealize = cc_keyboard_shortcut_editor_unrealize;
|
|
|
|
dialog_class->close = cc_keyboard_shortcut_editor_close;
|
|
dialog_class->response = cc_keyboard_shortcut_editor_response;
|
|
|
|
/**
|
|
* CcKeyboardShortcutEditor:keyboard-item:
|
|
*
|
|
* The current keyboard shortcut being edited.
|
|
*/
|
|
properties[PROP_KEYBOARD_ITEM] = g_param_spec_object ("keyboard-item",
|
|
"Keyboard item",
|
|
"The keyboard item being edited",
|
|
CC_TYPE_KEYBOARD_ITEM,
|
|
G_PARAM_READWRITE);
|
|
|
|
/**
|
|
* CcKeyboardShortcutEditor:panel:
|
|
*
|
|
* The current keyboard panel.
|
|
*/
|
|
properties[PROP_MANAGER] = g_param_spec_object ("manager",
|
|
"Keyboard manager",
|
|
"The keyboard manager",
|
|
CC_TYPE_KEYBOARD_MANAGER,
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
|
|
|
|
g_object_class_install_properties (object_class, N_PROPS, properties);
|
|
|
|
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/keyboard/cc-keyboard-shortcut-editor.ui");
|
|
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, add_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, cancel_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, change_custom_shortcut_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, command_entry);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, custom_grid);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, custom_shortcut_accel_label);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, custom_shortcut_stack);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, edit_box);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, headerbar);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, name_entry);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, new_shortcut_conflict_label);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, remove_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, replace_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, reset_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, reset_custom_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, set_button);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, shortcut_accel_label);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, shortcut_conflict_label);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, standard_box);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, stack);
|
|
gtk_widget_class_bind_template_child (widget_class, CcKeyboardShortcutEditor, top_info_label);
|
|
|
|
gtk_widget_class_bind_template_callback (widget_class, add_button_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, change_custom_shortcut_button_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, command_entry_changed_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, name_entry_changed_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, on_key_pressed_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, remove_button_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, replace_button_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, reset_custom_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, reset_item_clicked_cb);
|
|
gtk_widget_class_bind_template_callback (widget_class, set_button_clicked_cb);
|
|
}
|
|
|
|
static void
|
|
cc_keyboard_shortcut_editor_init (CcKeyboardShortcutEditor *self)
|
|
{
|
|
gtk_widget_init_template (GTK_WIDGET (self));
|
|
|
|
self->mode = CC_SHORTCUT_EDITOR_EDIT;
|
|
self->custom_is_modifier = TRUE;
|
|
self->custom_combo = g_new0 (CcKeyCombo, 1);
|
|
|
|
gtk_widget_set_direction (GTK_WIDGET (self->custom_shortcut_accel_label), GTK_TEXT_DIR_LTR);
|
|
gtk_widget_set_direction (GTK_WIDGET (self->shortcut_accel_label), GTK_TEXT_DIR_LTR);
|
|
}
|
|
|
|
/**
|
|
* cc_keyboard_shortcut_editor_new:
|
|
*
|
|
* Creates a new #CcKeyboardShortcutEditor.
|
|
*
|
|
* Returns: (transfer full): a newly created #CcKeyboardShortcutEditor.
|
|
*/
|
|
GtkWidget*
|
|
cc_keyboard_shortcut_editor_new (CcKeyboardManager *manager)
|
|
{
|
|
return g_object_new (CC_TYPE_KEYBOARD_SHORTCUT_EDITOR,
|
|
"manager", manager,
|
|
"use-header-bar", 1,
|
|
NULL);
|
|
}
|
|
|
|
/**
|
|
* cc_keyboard_shortcut_editor_get_item:
|
|
* @self: a #CcKeyboardShortcutEditor
|
|
*
|
|
* Retrieves the current keyboard shortcut being edited.
|
|
*
|
|
* Returns: (transfer none)(nullable): a #CcKeyboardItem
|
|
*/
|
|
CcKeyboardItem*
|
|
cc_keyboard_shortcut_editor_get_item (CcKeyboardShortcutEditor *self)
|
|
{
|
|
g_return_val_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self), NULL);
|
|
|
|
return self->item;
|
|
}
|
|
|
|
/**
|
|
* cc_keyboard_shortcut_editor_set_item:
|
|
* @self: a #CcKeyboardShortcutEditor
|
|
* @item: a #CcKeyboardItem
|
|
*
|
|
* Sets the current keyboard shortcut to be edited.
|
|
*/
|
|
void
|
|
cc_keyboard_shortcut_editor_set_item (CcKeyboardShortcutEditor *self,
|
|
CcKeyboardItem *item)
|
|
{
|
|
g_return_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self));
|
|
|
|
setup_keyboard_item (self, item);
|
|
|
|
if (!g_set_object (&self->item, item))
|
|
return;
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_KEYBOARD_ITEM]);
|
|
}
|
|
|
|
CcShortcutEditorMode
|
|
cc_keyboard_shortcut_editor_get_mode (CcKeyboardShortcutEditor *self)
|
|
{
|
|
g_return_val_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self), 0);
|
|
|
|
return self->mode;
|
|
}
|
|
|
|
void
|
|
cc_keyboard_shortcut_editor_set_mode (CcKeyboardShortcutEditor *self,
|
|
CcShortcutEditorMode mode)
|
|
{
|
|
gboolean is_create_mode;
|
|
|
|
g_return_if_fail (CC_IS_KEYBOARD_SHORTCUT_EDITOR (self));
|
|
|
|
self->mode = mode;
|
|
is_create_mode = mode == CC_SHORTCUT_EDITOR_CREATE;
|
|
|
|
gtk_widget_set_visible (GTK_WIDGET (self->new_shortcut_conflict_label), is_create_mode);
|
|
gtk_stack_set_visible_child (self->custom_shortcut_stack,
|
|
is_create_mode ? GTK_WIDGET (self->change_custom_shortcut_button) : GTK_WIDGET (self->custom_shortcut_accel_label));
|
|
|
|
if (mode == CC_SHORTCUT_EDITOR_CREATE)
|
|
{
|
|
/* Cleanup whatever was set before */
|
|
clear_custom_entries (self);
|
|
|
|
set_header_mode (self, HEADER_MODE_ADD);
|
|
set_shortcut_editor_page (self, PAGE_CUSTOM);
|
|
gtk_window_set_title (GTK_WINDOW (self), _("Add Custom Shortcut"));
|
|
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->command_entry), TRUE);
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->name_entry), TRUE);
|
|
gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
|
|
|
|
gtk_widget_hide (GTK_WIDGET (self->reset_custom_button));
|
|
}
|
|
}
|