aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/gsr-shortcut-accel-dialog.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gsr-shortcut-accel-dialog.c')
-rw-r--r--src/gsr-shortcut-accel-dialog.c372
1 files changed, 372 insertions, 0 deletions
diff --git a/src/gsr-shortcut-accel-dialog.c b/src/gsr-shortcut-accel-dialog.c
new file mode 100644
index 0000000..bf5b0e9
--- /dev/null
+++ b/src/gsr-shortcut-accel-dialog.c
@@ -0,0 +1,372 @@
+#include "gsr-shortcut-accel-dialog.h"
+
+#include <gdk/gdk.h>
+
+/* ═══════════════════════════════════════════════════════════════════
+ * GsrShortcutAccelDialog — Ptyxis-style key capture dialog
+ *
+ * Opens in "editing" (capture) mode. A GtkEventControllerKey in
+ * capture phase intercepts all key events.
+ *
+ * - Escape (no modifiers) → cancel / close
+ * - Backspace (no modifiers) → clear shortcut, emit "shortcut-set"
+ * - Valid key combo → store, switch to display mode, user clicks "Set"
+ *
+ * The dialog emits "shortcut-set" with the accelerator string (or NULL
+ * if cleared).
+ * ═══════════════════════════════════════════════════════════════════ */
+
+struct _GsrShortcutAccelDialog {
+ AdwDialog parent_instance;
+
+ /* Title of the shortcut being edited (e.g. "Start/Stop streaming") */
+ char *shortcut_title;
+
+ /* Current accelerator from config (for Reset) */
+ char *initial_accel;
+
+ /* Captured accelerator */
+ char *accelerator;
+ guint keyval;
+ GdkModifierType modifier;
+
+ /* State */
+ gboolean editing; /* TRUE = waiting for key press */
+
+ /* Widgets */
+ GtkStack *stack; /* "capture" / "display" pages */
+ GtkLabel *capture_label;
+ GtkLabel *capture_hint;
+ GtkShortcutLabel *display_label;
+ GtkButton *set_button;
+ GtkButton *cancel_button;
+};
+
+G_DEFINE_FINAL_TYPE(GsrShortcutAccelDialog, gsr_shortcut_accel_dialog, ADW_TYPE_DIALOG)
+
+/* ── Signals ─────────────────────────────────────────────────────── */
+
+enum {
+ SIGNAL_SHORTCUT_SET,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+/* ── Helpers ─────────────────────────────────────────────────────── */
+
+static void
+update_display(GsrShortcutAccelDialog *self)
+{
+ if (self->editing) {
+ gtk_stack_set_visible_child_name(self->stack, "capture");
+ gtk_widget_set_sensitive(GTK_WIDGET(self->set_button), FALSE);
+ } else {
+ if (self->accelerator) {
+ gtk_shortcut_label_set_accelerator(self->display_label,
+ self->accelerator);
+ } else {
+ gtk_shortcut_label_set_accelerator(self->display_label, "");
+ }
+ gtk_stack_set_visible_child_name(self->stack, "display");
+ gtk_widget_set_sensitive(GTK_WIDGET(self->set_button), TRUE);
+ }
+}
+
+static void
+set_accelerator(GsrShortcutAccelDialog *self, const char *accel)
+{
+ g_free(self->accelerator);
+ self->accelerator = g_strdup(accel);
+}
+
+/**
+ * Sanitize modifier mask: only keep standard modifier bits.
+ */
+static GdkModifierType
+sanitize_modifier_mask(GdkModifierType state)
+{
+ return state & gtk_accelerator_get_default_mod_mask();
+}
+
+/**
+ * Check if shift should be dropped (the key is the same with or without shift).
+ */
+static gboolean
+should_drop_shift(guint keyval_lower, guint keyval)
+{
+ /* If lowering the key didn't change it, shift isn't meaningful for this key */
+ return keyval_lower == keyval;
+}
+
+/* ── Key event handlers ──────────────────────────────────────────── */
+
+static gboolean
+on_key_pressed(GtkEventControllerKey *controller,
+ guint keyval,
+ guint keycode G_GNUC_UNUSED,
+ GdkModifierType state,
+ gpointer user_data)
+{
+ GsrShortcutAccelDialog *self = GSR_SHORTCUT_ACCEL_DIALOG(user_data);
+
+ if (!self->editing)
+ return GDK_EVENT_PROPAGATE;
+
+ GdkEvent *event = gtk_event_controller_get_current_event(
+ GTK_EVENT_CONTROLLER(controller));
+
+ /* Ignore pure modifier key presses */
+ if (gdk_key_event_is_modifier(event))
+ return GDK_EVENT_PROPAGATE;
+
+ GdkModifierType real_mask = sanitize_modifier_mask(state);
+ guint keyval_lower = gdk_keyval_to_lower(keyval);
+
+ /* Normalize ISO_Left_Tab → Tab */
+ if (keyval_lower == GDK_KEY_ISO_Left_Tab)
+ keyval_lower = GDK_KEY_Tab;
+
+ /* Put shift back if it changed the case of the key */
+ if (keyval_lower != keyval)
+ real_mask |= GDK_SHIFT_MASK;
+
+ /* Avoid SysRq translation (keep Alt+Print) */
+ if (keyval_lower == GDK_KEY_Sys_Req && (real_mask & GDK_ALT_MASK) != 0)
+ keyval_lower = GDK_KEY_Print;
+
+ /* Escape with no modifiers = cancel */
+ if (real_mask == 0 && keyval_lower == GDK_KEY_Escape) {
+ adw_dialog_close(ADW_DIALOG(self));
+ return GDK_EVENT_STOP;
+ }
+
+ /* Backspace with no modifiers = clear shortcut */
+ if (real_mask == 0 && keyval_lower == GDK_KEY_BackSpace) {
+ set_accelerator(self, NULL);
+ self->keyval = 0;
+ self->modifier = 0;
+ g_signal_emit(self, signals[SIGNAL_SHORTCUT_SET], 0);
+ adw_dialog_close(ADW_DIALOG(self));
+ return GDK_EVENT_STOP;
+ }
+
+ /* Store the captured shortcut */
+ self->keyval = gdk_keyval_to_lower(keyval);
+ self->modifier = sanitize_modifier_mask(state);
+
+ /* Handle shift normalization */
+ if ((state & GDK_SHIFT_MASK) != 0 && should_drop_shift(self->keyval, keyval))
+ self->modifier &= ~GDK_SHIFT_MASK;
+ if ((state & GDK_LOCK_MASK) == 0 && self->keyval != keyval)
+ self->modifier |= GDK_SHIFT_MASK;
+
+ /* Normalize Ctrl+ISO_Left_Tab → Ctrl+Shift+Tab */
+ if (self->keyval == GDK_KEY_ISO_Left_Tab &&
+ self->modifier == GDK_CONTROL_MASK)
+ {
+ self->keyval = GDK_KEY_Tab;
+ self->modifier = GDK_CONTROL_MASK | GDK_SHIFT_MASK;
+ }
+
+ /* Build accelerator string */
+ g_free(self->accelerator);
+ self->accelerator = gtk_accelerator_name(self->keyval, self->modifier);
+
+ /* Switch to display mode */
+ self->editing = FALSE;
+ update_display(self);
+ gtk_widget_grab_focus(GTK_WIDGET(self->set_button));
+
+ return GDK_EVENT_STOP;
+}
+
+/* ── Button callbacks ────────────────────────────────────────────── */
+
+static void
+on_set_clicked(GtkButton *btn G_GNUC_UNUSED, gpointer user_data)
+{
+ GsrShortcutAccelDialog *self = GSR_SHORTCUT_ACCEL_DIALOG(user_data);
+ g_signal_emit(self, signals[SIGNAL_SHORTCUT_SET], 0);
+ adw_dialog_close(ADW_DIALOG(self));
+}
+
+static void
+on_cancel_clicked(GtkButton *btn G_GNUC_UNUSED, gpointer user_data)
+{
+ GsrShortcutAccelDialog *self = GSR_SHORTCUT_ACCEL_DIALOG(user_data);
+ adw_dialog_close(ADW_DIALOG(self));
+}
+
+/* ── GObject lifecycle ───────────────────────────────────────────── */
+
+static void
+gsr_shortcut_accel_dialog_finalize(GObject *object)
+{
+ GsrShortcutAccelDialog *self = GSR_SHORTCUT_ACCEL_DIALOG(object);
+ g_free(self->shortcut_title);
+ g_free(self->initial_accel);
+ g_free(self->accelerator);
+ G_OBJECT_CLASS(gsr_shortcut_accel_dialog_parent_class)->finalize(object);
+}
+
+static void
+gsr_shortcut_accel_dialog_init(GsrShortcutAccelDialog *self)
+{
+ (void)self;
+}
+
+static void
+gsr_shortcut_accel_dialog_class_init(GsrShortcutAccelDialogClass *klass)
+{
+ GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+ obj_class->finalize = gsr_shortcut_accel_dialog_finalize;
+
+ /**
+ * GsrShortcutAccelDialog::shortcut-set:
+ *
+ * Emitted when the user confirms a new shortcut (Set button)
+ * or clears it (Backspace). Call get_accelerator() to retrieve
+ * the result.
+ */
+ signals[SIGNAL_SHORTCUT_SET] = g_signal_new(
+ "shortcut-set",
+ G_TYPE_FROM_CLASS(klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+/* ── Build UI ────────────────────────────────────────────────────── */
+
+static void
+build_dialog_ui(GsrShortcutAccelDialog *self)
+{
+ adw_dialog_set_title(ADW_DIALOG(self), "Set Shortcut");
+ adw_dialog_set_content_width(ADW_DIALOG(self), 400);
+ adw_dialog_set_content_height(ADW_DIALOG(self), 260);
+
+ /* ── Header bar ─── */
+ AdwHeaderBar *header = ADW_HEADER_BAR(adw_header_bar_new());
+
+ self->cancel_button = GTK_BUTTON(gtk_button_new_with_label("Cancel"));
+ g_signal_connect(self->cancel_button, "clicked",
+ G_CALLBACK(on_cancel_clicked), self);
+ adw_header_bar_pack_start(header, GTK_WIDGET(self->cancel_button));
+
+ self->set_button = GTK_BUTTON(gtk_button_new_with_label("Set"));
+ gtk_widget_add_css_class(GTK_WIDGET(self->set_button), "suggested-action");
+ gtk_widget_set_sensitive(GTK_WIDGET(self->set_button), FALSE);
+ g_signal_connect(self->set_button, "clicked",
+ G_CALLBACK(on_set_clicked), self);
+ adw_header_bar_pack_end(header, GTK_WIDGET(self->set_button));
+
+ /* ── Content stack ─── */
+ self->stack = GTK_STACK(gtk_stack_new());
+ gtk_stack_set_transition_type(self->stack, GTK_STACK_TRANSITION_TYPE_CROSSFADE);
+
+ /* -- Capture page -- */
+ GtkBox *capture_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 12));
+ gtk_widget_set_halign(GTK_WIDGET(capture_box), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(capture_box), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(GTK_WIDGET(capture_box), 24);
+ gtk_widget_set_margin_end(GTK_WIDGET(capture_box), 24);
+ gtk_widget_set_margin_top(GTK_WIDGET(capture_box), 24);
+ gtk_widget_set_margin_bottom(GTK_WIDGET(capture_box), 24);
+
+ GtkImage *keyboard_icon = GTK_IMAGE(
+ gtk_image_new_from_icon_name("input-keyboard-symbolic"));
+ gtk_image_set_pixel_size(keyboard_icon, 64);
+ gtk_widget_add_css_class(GTK_WIDGET(keyboard_icon), "dim-label");
+ gtk_box_append(capture_box, GTK_WIDGET(keyboard_icon));
+
+ char *label_text = g_strdup_printf(
+ "Enter new shortcut for\n<b>%s</b>",
+ self->shortcut_title ? self->shortcut_title : "Shortcut");
+ self->capture_label = GTK_LABEL(gtk_label_new(NULL));
+ gtk_label_set_markup(self->capture_label, label_text);
+ gtk_label_set_justify(self->capture_label, GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(self->capture_label, TRUE);
+ g_free(label_text);
+ gtk_box_append(capture_box, GTK_WIDGET(self->capture_label));
+
+ self->capture_hint = GTK_LABEL(gtk_label_new(
+ "Press Escape to cancel or Backspace to disable the shortcut."));
+ gtk_widget_add_css_class(GTK_WIDGET(self->capture_hint), "dim-label");
+ gtk_label_set_wrap(self->capture_hint, TRUE);
+ gtk_label_set_justify(self->capture_hint, GTK_JUSTIFY_CENTER);
+ gtk_box_append(capture_box, GTK_WIDGET(self->capture_hint));
+
+ gtk_stack_add_named(self->stack, GTK_WIDGET(capture_box), "capture");
+
+ /* -- Display page -- */
+ GtkBox *display_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 12));
+ gtk_widget_set_halign(GTK_WIDGET(display_box), GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(GTK_WIDGET(display_box), GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(GTK_WIDGET(display_box), 24);
+ gtk_widget_set_margin_end(GTK_WIDGET(display_box), 24);
+ gtk_widget_set_margin_top(GTK_WIDGET(display_box), 24);
+ gtk_widget_set_margin_bottom(GTK_WIDGET(display_box), 24);
+
+ GtkLabel *display_title = GTK_LABEL(gtk_label_new(NULL));
+ char *display_text = g_strdup_printf(
+ "Shortcut for <b>%s</b>",
+ self->shortcut_title ? self->shortcut_title : "Shortcut");
+ gtk_label_set_markup(display_title, display_text);
+ gtk_label_set_justify(display_title, GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(display_title, TRUE);
+ g_free(display_text);
+ gtk_box_append(display_box, GTK_WIDGET(display_title));
+
+ self->display_label = GTK_SHORTCUT_LABEL(gtk_shortcut_label_new(""));
+ gtk_widget_set_halign(GTK_WIDGET(self->display_label), GTK_ALIGN_CENTER);
+ gtk_box_append(display_box, GTK_WIDGET(self->display_label));
+
+ gtk_stack_add_named(self->stack, GTK_WIDGET(display_box), "display");
+
+ /* ── Key event controller (capture phase) ─── */
+ GtkEventController *key_ctrl = gtk_event_controller_key_new();
+ gtk_event_controller_set_propagation_phase(key_ctrl, GTK_PHASE_CAPTURE);
+ g_signal_connect(key_ctrl, "key-pressed",
+ G_CALLBACK(on_key_pressed), self);
+ gtk_widget_add_controller(GTK_WIDGET(self), key_ctrl);
+
+ /* ── Layout: toolbar-view ─── */
+ AdwToolbarView *toolbar_view = ADW_TOOLBAR_VIEW(adw_toolbar_view_new());
+ adw_toolbar_view_add_top_bar(toolbar_view, GTK_WIDGET(header));
+ adw_toolbar_view_set_content(toolbar_view, GTK_WIDGET(self->stack));
+
+ adw_dialog_set_child(ADW_DIALOG(self), GTK_WIDGET(toolbar_view));
+
+ /* Start in capture mode */
+ self->editing = TRUE;
+ update_display(self);
+}
+
+/* ── Public API ──────────────────────────────────────────────────── */
+
+GsrShortcutAccelDialog *
+gsr_shortcut_accel_dialog_new(const char *shortcut_title,
+ const char *current_accel)
+{
+ GsrShortcutAccelDialog *self = g_object_new(
+ GSR_TYPE_SHORTCUT_ACCEL_DIALOG, NULL);
+
+ self->shortcut_title = g_strdup(shortcut_title);
+ self->initial_accel = g_strdup(current_accel);
+ self->accelerator = g_strdup(current_accel);
+ self->editing = TRUE;
+ self->keyval = 0;
+ self->modifier = 0;
+
+ build_dialog_ui(self);
+
+ return self;
+}
+
+const char *
+gsr_shortcut_accel_dialog_get_accelerator(GsrShortcutAccelDialog *self)
+{
+ g_return_val_if_fail(GSR_IS_SHORTCUT_ACCEL_DIALOG(self), NULL);
+ return self->accelerator;
+}