Files

189 lines
9.2 KiB
C

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
#include "native_internal.h"
#include <gdk/gdkx.h>
#include <xkbcommon/xkbcommon.h>
// The preferences window owns the accepted draft. Each recorder owns a fresh
// candidate and held modifier set; cancelling or destroying it discards both.
typedef struct {
GtkWidget *dialog;
GtkWidget *entry;
GPtrArray *keys;
GHashTable *modifiers;
} KeyRecorder;
static GPtrArray *interruption_keys(void) {
return g_object_get_data(G_OBJECT(prefs_widget("window")), "interruption-keys");
}
static char *key_label(const char *name) {
const struct { const char *name; const char *label; } labels[] = {
{"Control_L", "Left Control"}, {"Control_R", "Right Control"},
{"Shift_L", "Left Shift"}, {"Shift_R", "Right Shift"},
{"Alt_L", "Left Alt"}, {"Alt_R", "Right Alt"},
{"Super_L", "Left Super"}, {"Super_R", "Right Super"},
{"Return", "Enter"}, {"KP_Enter", "Numpad Enter"}, {"space", "Space"},
};
for (unsigned i = 0; i < G_N_ELEMENTS(labels); i++)
if (strcmp(name, labels[i].name) == 0) return g_strdup(labels[i].label);
guint keyval = xkb_keysym_from_name(name, XKB_KEYSYM_NO_FLAGS);
return keyval == XKB_KEY_NoSymbol ? g_strdup(name) : gtk_accelerator_get_label(keyval, 0);
}
static void show_keys(GtkWidget *entry, GPtrArray *keys) {
GString *text = g_string_new(NULL);
for (unsigned i = 0; i < keys->len; i++) {
char *label = key_label(g_ptr_array_index(keys, i));
if (i) g_string_append(text, ", ");
g_string_append(text, label);
g_free(label);
}
gtk_entry_set_text(GTK_ENTRY(entry), text->len ? text->str : "No keys defined");
g_string_free(text, TRUE);
}
void prefs_interruption_clear(void) {
g_ptr_array_set_size(interruption_keys(), 0);
show_keys(prefs_widget("interruptionKeysEntry"), interruption_keys());
}
void prefs_interruption_append(const char *name) {
g_ptr_array_add(interruption_keys(), g_strdup(name));
show_keys(prefs_widget("interruptionKeysEntry"), interruption_keys());
}
int prefs_interruption_count(void) { return interruption_keys()->len; }
const char *prefs_interruption_key(int index) { return g_ptr_array_index(interruption_keys(), index); }
static void interruption_sensitivity(GtkToggleButton *button, gpointer data) {
(void)button; (void)data;
gboolean custom = prefs_check_get("interruptCustomKeysRadio");
gtk_widget_set_sensitive(prefs_widget("recordInterruptionKeysButton"), custom);
gtk_widget_set_sensitive(prefs_widget("interruptionKeysEntry"), custom);
}
static void record_key(KeyRecorder *recorder, guint keyval) {
// GTK and X11 use different aliases for Page Up and multimedia keys.
// Persist XKB names, matching both input observers rather than GTK names.
char name[128];
int length = xkb_keysym_get_name(gdk_keyval_to_lower(keyval), name, sizeof(name));
if (length <= 0 || (unsigned)length >= sizeof(name)) return;
for (unsigned i = 0; i < recorder->keys->len; i++)
if (strcmp(name, g_ptr_array_index(recorder->keys, i)) == 0) return;
if (recorder->keys->len == 64) return;
g_ptr_array_add(recorder->keys, g_strdup(name));
show_keys(recorder->entry, recorder->keys);
gtk_dialog_set_response_sensitive(GTK_DIALOG(recorder->dialog), GTK_RESPONSE_OK, TRUE);
}
static guint recording_keyval(GdkEventKey *event) {
if (!GDK_IS_X11_DISPLAY(gdk_display_get_default())) return event->keyval;
// Match the base names published by the X11 observer, including keypad
// keys with Num Lock enabled. This records individual keys, not chords.
GdkKeymapKey key = {event->hardware_keycode, 0, 0};
guint keyval = gdk_keymap_lookup_key(gdk_keymap_get_for_display(gdk_display_get_default()), &key);
return keyval ? keyval : event->keyval;
}
static gboolean recording_key(GtkWidget *dialog, GdkEventKey *event, gpointer data) {
KeyRecorder *recorder = data;
guint state = event->state & gtk_accelerator_get_default_mod_mask();
gboolean finish = state == GDK_CONTROL_MASK &&
(event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter);
gboolean cancel = state == GDK_CONTROL_MASK && event->keyval == GDK_KEY_Escape;
if (event->type == GDK_KEY_PRESS && (finish || cancel)) {
g_hash_table_remove_all(recorder->modifiers);
if (cancel || recorder->keys->len)
gtk_dialog_response(GTK_DIALOG(dialog), cancel ? GTK_RESPONSE_CANCEL : GTK_RESPONSE_OK);
else
gtk_entry_set_text(GTK_ENTRY(recorder->entry), "Record at least one key, then press Control+Enter.");
return TRUE;
}
guint keyval = recording_keyval(event);
if (event->is_modifier) {
// Record modifiers on release so the Control used to finish or cancel
// never becomes an interruption key just because it ended recording.
if (event->type == GDK_KEY_PRESS)
g_hash_table_add(recorder->modifiers, GUINT_TO_POINTER(keyval));
else if (g_hash_table_remove(recorder->modifiers, GUINT_TO_POINTER(keyval)))
record_key(recorder, keyval);
} else if (event->type == GDK_KEY_PRESS) {
record_key(recorder, keyval);
}
// Consume Tab, Enter, Escape and Space before GTK focus/default actions.
return TRUE;
}
static gboolean recording_focus_out(GtkWidget *dialog, GdkEventFocus *event, gpointer data) {
(void)dialog; (void)event;
KeyRecorder *recorder = data;
g_hash_table_remove_all(recorder->modifiers);
return FALSE;
}
static void free_recorder(gpointer data) {
KeyRecorder *recorder = data;
g_ptr_array_unref(recorder->keys);
g_hash_table_unref(recorder->modifiers);
g_free(recorder);
}
static void recording_response(GtkDialog *dialog, int response, gpointer data) {
KeyRecorder *recorder = data;
if (response == GTK_RESPONSE_OK && recorder->keys->len) {
prefs_interruption_clear();
for (unsigned i = 0; i < recorder->keys->len; i++)
prefs_interruption_append(g_ptr_array_index(recorder->keys, i));
}
gtk_widget_destroy(GTK_WIDGET(dialog));
gtk_window_present(GTK_WINDOW(prefs_widget("window")));
gtk_widget_grab_focus(prefs_widget("recordInterruptionKeysButton"));
}
static void start_recording(GtkButton *button, gpointer data) {
(void)button; (void)data;
const char *instructions = "Press each key that should interrupt speech. "
"Tab, Enter, Space, and Escape can be recorded. "
"Press and release a modifier by itself to record it. "
"Control+Enter finishes; Control+Escape cancels. "
"Recording replaces the previous keys. Up to 64 keys can be recorded.";
KeyRecorder *recorder = g_new0(KeyRecorder, 1);
recorder->keys = g_ptr_array_new_with_free_func(g_free);
recorder->modifiers = g_hash_table_new(g_direct_hash, g_direct_equal);
recorder->dialog = gtk_dialog_new_with_buttons("Record interruption keys",
GTK_WINDOW(prefs_widget("window")), GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
"Cancel recording", GTK_RESPONSE_CANCEL, "Finish recording", GTK_RESPONSE_OK, NULL);
g_object_set_data_full(G_OBJECT(recorder->dialog), "key-recorder", recorder, free_recorder);
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(recorder->dialog));
gtk_container_set_border_width(GTK_CONTAINER(content), 12);
GtkWidget *label = gtk_label_new(instructions);
gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(label), 65);
gtk_box_pack_start(GTK_BOX(content), label, FALSE, FALSE, 8);
recorder->entry = gtk_entry_new();
gtk_editable_set_editable(GTK_EDITABLE(recorder->entry), FALSE);
atk_object_set_name(gtk_widget_get_accessible(recorder->entry), instructions);
atk_object_set_description(gtk_widget_get_accessible(recorder->dialog), instructions);
gtk_box_pack_start(GTK_BOX(content), recorder->entry, FALSE, FALSE, 8);
show_keys(recorder->entry, recorder->keys);
gtk_dialog_set_response_sensitive(GTK_DIALOG(recorder->dialog), GTK_RESPONSE_OK, FALSE);
g_signal_connect(recorder->dialog, "key-press-event", G_CALLBACK(recording_key), recorder);
g_signal_connect(recorder->dialog, "key-release-event", G_CALLBACK(recording_key), recorder);
g_signal_connect(recorder->dialog, "focus-out-event", G_CALLBACK(recording_focus_out), recorder);
g_signal_connect(recorder->dialog, "response", G_CALLBACK(recording_response), recorder);
gtk_widget_show_all(recorder->dialog);
gtk_widget_grab_focus(recorder->entry);
}
void prefs_init_interruption(void) {
g_object_set_data_full(G_OBJECT(prefs_widget("window")), "interruption-keys",
g_ptr_array_new_with_free_func(g_free), (GDestroyNotify)g_ptr_array_unref);
g_signal_connect(prefs_widget("interruptCustomKeysRadio"), "toggled", G_CALLBACK(interruption_sensitivity), NULL);
g_signal_connect(prefs_widget("recordInterruptionKeysButton"), "clicked", G_CALLBACK(start_recording), NULL);
atk_object_set_description(gtk_widget_get_accessible(prefs_widget("recordInterruptionKeysButton")),
"Record individual keys. Control+Enter finishes; Control+Escape cancels.");
atk_object_set_name(gtk_widget_get_accessible(prefs_widget("interruptionKeysEntry")), "Defined interruption keys");
interruption_sensitivity(NULL, NULL);
}