189 lines
9.2 KiB
C
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);
|
|
}
|