Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "native_internal.h"
|
||||
|
||||
static GtkBuilder *builder;
|
||||
static int action;
|
||||
static int voice_changed;
|
||||
// Borrowed from the builder for the duration of one Apply/OK request.
|
||||
static GtkWidget *busy_focus;
|
||||
void prefs_set_action(int value) { action = value; }
|
||||
GtkWidget *prefs_widget(const char *id) { return GTK_WIDGET(gtk_builder_get_object(builder, id)); }
|
||||
static void name_accessible_tree(AtkObject *object, const char *name) {
|
||||
const char *current = atk_object_get_name(object);
|
||||
if (!current || !current[0]) atk_object_set_name(object, name);
|
||||
int children = atk_object_get_n_accessible_children(object);
|
||||
for (int i = 0; i < children; i++) {
|
||||
AtkObject *child = atk_object_ref_accessible_child(object, i);
|
||||
if (!child) continue;
|
||||
name_accessible_tree(child, name);
|
||||
g_object_unref(child);
|
||||
}
|
||||
}
|
||||
static void name_widget(const char *id, const char *name) {
|
||||
name_accessible_tree(gtk_widget_get_accessible(prefs_widget(id)), name);
|
||||
}
|
||||
static void describe_combo_tree(AtkObject *object, const char *name, const char *value) {
|
||||
AtkRole role = atk_object_get_role(object);
|
||||
if (role == ATK_ROLE_COMBO_BOX || role == ATK_ROLE_PUSH_BUTTON ||
|
||||
role == ATK_ROLE_TOGGLE_BUTTON) {
|
||||
atk_object_set_name(object, name);
|
||||
atk_object_set_description(object, value);
|
||||
}
|
||||
int children = atk_object_get_n_accessible_children(object);
|
||||
for (int i = 0; i < children; i++) {
|
||||
AtkObject *child = atk_object_ref_accessible_child(object, i);
|
||||
if (!child) continue;
|
||||
describe_combo_tree(child, name, value);
|
||||
g_object_unref(child);
|
||||
}
|
||||
}
|
||||
static void combo_changed(GtkComboBox *combo, gpointer data) {
|
||||
gchar *value = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(combo));
|
||||
describe_combo_tree(gtk_widget_get_accessible(GTK_WIDGET(combo)),
|
||||
(const char *)data, value ? value : "");
|
||||
g_free(value);
|
||||
}
|
||||
static void free_signal_data(gpointer data, GClosure *closure) {
|
||||
(void)closure;
|
||||
g_free(data);
|
||||
}
|
||||
static void set_focus_chain(const char *container_id, const char *ids[], unsigned int count) {
|
||||
GList *chain = NULL;
|
||||
for (unsigned int i = 0; i < count; i++) chain = g_list_append(chain, prefs_widget(ids[i]));
|
||||
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
|
||||
gtk_container_set_focus_chain(GTK_CONTAINER(prefs_widget(container_id)), chain);
|
||||
G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
g_list_free(chain);
|
||||
}
|
||||
static void clicked(GtkButton *button, gpointer data) {
|
||||
action = GPOINTER_TO_INT(data);
|
||||
if (action == 1 || action == 2) busy_focus = GTK_WIDGET(button);
|
||||
}
|
||||
static gboolean closing(GtkWidget *window, GdkEvent *event, gpointer data) { action = 3; return TRUE; }
|
||||
static gboolean key(GtkWidget *window, GdkEventKey *event, gpointer data) {
|
||||
if (event->keyval == GDK_KEY_Escape) { action = 3; return TRUE; }
|
||||
return FALSE;
|
||||
}
|
||||
static void defaults(GtkToggleButton *button, gpointer spin) {
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(spin), !gtk_toggle_button_get_active(button));
|
||||
}
|
||||
static void voice_value_changed(GtkWidget *control, gpointer data) {
|
||||
(void)control; (void)data;
|
||||
voice_changed = 1;
|
||||
}
|
||||
static gboolean voice_scale_key_press(GtkWidget *control, GdkEventKey *event, gpointer data) {
|
||||
(void)data;
|
||||
if (event->state & gtk_accelerator_get_default_mod_mask()) return FALSE;
|
||||
double step;
|
||||
switch (event->keyval) {
|
||||
case GDK_KEY_Up: case GDK_KEY_KP_Up: step = 1.0; break;
|
||||
case GDK_KEY_Down: case GDK_KEY_KP_Down: step = -1.0; break;
|
||||
case GDK_KEY_Right: case GDK_KEY_KP_Right: step = 0.1; break;
|
||||
case GDK_KEY_Left: case GDK_KEY_KP_Left: step = -0.1; break;
|
||||
default: return FALSE;
|
||||
}
|
||||
// GtkRange clamps at the adjustment's bounds and emits value-changed,
|
||||
// preserving accessible value updates and the existing preview path.
|
||||
GtkRange *range = GTK_RANGE(control);
|
||||
gtk_range_set_value(range, gtk_range_get_value(range) + step);
|
||||
return TRUE;
|
||||
}
|
||||
static void echo_sensitivity(GtkToggleButton *button, gpointer data) {
|
||||
(void)button; (void)data;
|
||||
int enabled = prefs_check_get("keyEchoCheckButton");
|
||||
const char *categories[] = {"enableAlphabeticKeysCheckButton", "enableNumericKeysCheckButton",
|
||||
"enablePunctuationKeysCheckButton", "enableSpaceCheckButton", "enableModifierKeysCheckButton",
|
||||
"enableFunctionKeysCheckButton", "enableActionKeysCheckButton", "enableNavigationKeysCheckButton",
|
||||
"enableDiacriticalKeysCheckButton"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(categories); i++) gtk_widget_set_sensitive(prefs_widget(categories[i]), enabled);
|
||||
int custom = prefs_check_get("useCustomEchoVoiceCheckButton");
|
||||
const char *voice[] = {"useCustomEchoSpeechServerCheckButton", "echoSpeechFamilies", "echoRateScale", "echoPitchScale", "echoVolumeScale"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(voice); i++) gtk_widget_set_sensitive(prefs_widget(voice[i]), custom);
|
||||
gtk_widget_set_sensitive(prefs_widget("echoSpeechServers"), custom && prefs_check_get("useCustomEchoSpeechServerCheckButton"));
|
||||
gtk_widget_set_sensitive(prefs_widget("useCustomEchoForKeyCheckButton"), custom && enabled);
|
||||
gtk_widget_set_sensitive(prefs_widget("useCustomEchoForCharacterCheckButton"), custom && prefs_check_get("enableEchoByCharacterCheckButton"));
|
||||
gtk_widget_set_sensitive(prefs_widget("useCustomEchoForWordCheckButton"), custom && prefs_check_get("enableEchoByWordCheckButton"));
|
||||
gtk_widget_set_sensitive(prefs_widget("useCustomEchoForSentenceCheckButton"), custom && prefs_check_get("enableEchoBySentenceCheckButton"));
|
||||
}
|
||||
static void sound_sensitivity(GtkToggleButton *button, gpointer data) {
|
||||
(void)button; (void)data;
|
||||
int enabled = prefs_check_get("enableSoundCheckButton");
|
||||
const char *controls[] = {"soundVolumeScale", "soundSinkCombo", "soundThemeCombo",
|
||||
"roleSoundPresentationCombo", "playSoundForRoleCheckButton", "playSoundForStateCheckButton",
|
||||
"playSoundForPositionInSetCheckButton", "spatializeObjectSoundsCheckButton"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(controls); i++) gtk_widget_set_sensitive(prefs_widget(controls[i]), enabled);
|
||||
}
|
||||
static void ocr_sensitivity(GtkToggleButton *button, gpointer data) {
|
||||
(void)button; (void)data;
|
||||
gtk_widget_set_sensitive(prefs_widget("ocrBlackWhiteValueSpinButton"), prefs_check_get("ocrBlackWhiteCheckButton"));
|
||||
gtk_widget_set_sensitive(prefs_widget("ocrRefreshIntervalSpinButton"), prefs_check_get("ocrAutoRefreshCheckButton"));
|
||||
}
|
||||
int prefs_init(const char *xml) {
|
||||
if (!gtk_init_check(NULL, NULL)) return 0;
|
||||
g_set_application_name("Cthulhu Preferences");
|
||||
builder = gtk_builder_new();
|
||||
GError *error = NULL;
|
||||
if (!gtk_builder_add_from_string(builder, xml, -1, &error)) {
|
||||
if (error) g_error_free(error);
|
||||
g_object_unref(builder); builder = NULL;
|
||||
return 0;
|
||||
}
|
||||
prefs_init_reading_settings();
|
||||
prefs_init_key_bindings();
|
||||
prefs_init_modifiers();
|
||||
prefs_init_pronunciation();
|
||||
prefs_init_text_attributes();
|
||||
prefs_init_tentacles();
|
||||
AtkObject *reading_accessible = gtk_widget_get_accessible(prefs_widget("readingSettingsTreeview"));
|
||||
atk_object_set_name(reading_accessible, "Reading settings");
|
||||
atk_object_set_description(reading_accessible, "Use arrow keys to move between settings and Space to toggle the selected setting.");
|
||||
g_signal_connect(prefs_widget("apply"), "clicked", G_CALLBACK(clicked), GINT_TO_POINTER(1));
|
||||
g_signal_connect(prefs_widget("save"), "clicked", G_CALLBACK(clicked), GINT_TO_POINTER(2));
|
||||
g_signal_connect(prefs_widget("cancel"), "clicked", G_CALLBACK(clicked), GINT_TO_POINTER(3));
|
||||
g_signal_connect(prefs_widget("window"), "delete-event", G_CALLBACK(closing), NULL);
|
||||
g_signal_connect(prefs_widget("window"), "key-press-event", G_CALLBACK(key), NULL);
|
||||
const char *ids[] = {"rate", "pitch", "volume"};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
gchar *id = g_strconcat(ids[i], "_default", NULL);
|
||||
g_signal_connect(prefs_widget(id), "toggled", G_CALLBACK(defaults), prefs_widget(ids[i]));
|
||||
g_signal_connect(prefs_widget(id), "toggled", G_CALLBACK(voice_value_changed), NULL);
|
||||
g_signal_connect(prefs_widget(ids[i]), "value-changed", G_CALLBACK(voice_value_changed), NULL);
|
||||
g_free(id);
|
||||
}
|
||||
const char *voiceScales[] = {"rate", "pitch", "volume", "echoRateScale", "echoPitchScale", "echoVolumeScale"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(voiceScales); i++) {
|
||||
GtkWidget *control = prefs_widget(voiceScales[i]);
|
||||
g_signal_connect(control, "key-press-event", G_CALLBACK(voice_scale_key_press), NULL);
|
||||
atk_object_set_description(gtk_widget_get_accessible(control),
|
||||
"Up and Down change by 1; Left and Right change by 0.1.");
|
||||
}
|
||||
const char *voiceCombos[] = {
|
||||
"speechSystems", "speechServers", "speechLanguages", "speechFamilies", "voiceTypesCombo", "hardwareBaudCombo",
|
||||
"echoSpeechServers", "echoSpeechFamilies",
|
||||
};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(voiceCombos); i++) {
|
||||
g_signal_connect(prefs_widget(voiceCombos[i]), "changed",
|
||||
G_CALLBACK(voice_value_changed), NULL);
|
||||
}
|
||||
g_signal_connect(prefs_widget("hardwareDeviceEntry"), "changed", G_CALLBACK(voice_value_changed), NULL);
|
||||
GtkEntryCompletion *completion = gtk_entry_completion_new();
|
||||
GtkListStore *devices = gtk_list_store_new(1, G_TYPE_STRING);
|
||||
gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(devices));
|
||||
gtk_entry_completion_set_text_column(completion, 0);
|
||||
gtk_entry_completion_set_inline_completion(completion, TRUE);
|
||||
gtk_entry_set_completion(GTK_ENTRY(prefs_widget("hardwareDeviceEntry")), completion);
|
||||
g_object_unref(completion);
|
||||
g_object_unref(devices);
|
||||
const char *labelled[] = {"rate", "pitch", "volume", "capitalization"};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
gchar *id = g_strconcat(labelled[i], "_label", NULL);
|
||||
name_widget(labelled[i], gtk_label_get_text(GTK_LABEL(prefs_widget(id))));
|
||||
g_free(id);
|
||||
}
|
||||
const struct { const char *id; const char *name; } named[] = {
|
||||
{"timeFormatCombo", "Time format"},
|
||||
{"dateFormatCombo", "Date format"},
|
||||
{"progressBarOutputCombo", "Progress bar output"},
|
||||
{"sayAllStyle", "Say All by"},
|
||||
{"soundVolumeScale", "Sound volume"},
|
||||
{"soundSinkCombo", "Audio backend"},
|
||||
{"soundThemeCombo", "Sound theme"},
|
||||
{"roleSoundPresentationCombo", "Element presentation"},
|
||||
{"speechSystems", "Speech system"},
|
||||
{"speechServers", "Speech synthesizer"},
|
||||
{"speechLanguages", "Language"},
|
||||
{"speechFamilies", "Person"},
|
||||
{"speechHistoryLimitSpinButton", "Speech history entries"},
|
||||
{"voiceTypesCombo", "Voice type"},
|
||||
{"capitalization", "Capitalization style"},
|
||||
{"hardwareDeviceEntry", "Serial device"},
|
||||
{"hardwareBaudCombo", "Serial baud rate"},
|
||||
{"contractionTableCombo", "Contraction table"},
|
||||
{"brailleFlashTimeSpinButton", "Flash message duration"},
|
||||
{"echoSpeechServers", "Key echo speech synthesizer"},
|
||||
{"echoSpeechFamilies", "Key echo person"},
|
||||
{"echoRateScale", "Key echo rate"},
|
||||
{"echoPitchScale", "Key echo pitch"},
|
||||
{"echoVolumeScale", "Key echo volume"},
|
||||
{"keyBindingEntry", "Shortcuts"},
|
||||
{"ocrScaleSpinButton", "OCR scale factor"},
|
||||
{"ocrBlackWhiteValueSpinButton", "Black and white threshold"},
|
||||
{"ocrLanguageEntry", "OCR recognition languages"},
|
||||
{"ocrLayoutCombo", "OCR text layout"},
|
||||
{"ocrRefreshIntervalSpinButton", "OCR refresh interval in milliseconds"},
|
||||
};
|
||||
for (unsigned int i = 0; i < G_N_ELEMENTS(named); i++) {
|
||||
name_widget(named[i].id, named[i].name);
|
||||
GtkWidget *control = prefs_widget(named[i].id);
|
||||
if (GTK_IS_COMBO_BOX(control) &&
|
||||
gtk_combo_box_get_active(GTK_COMBO_BOX(control)) < 0) {
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(control), 0);
|
||||
}
|
||||
if (GTK_IS_COMBO_BOX_TEXT(control)) {
|
||||
g_signal_connect_data(control, "changed", G_CALLBACK(combo_changed),
|
||||
g_strdup(named[i].name), free_signal_data, 0);
|
||||
}
|
||||
}
|
||||
const char *keyboard_focus[] = {
|
||||
"generalDesktopButton", "generalLaptopButton",
|
||||
};
|
||||
set_focus_chain("keyboard_layout_controls", keyboard_focus,
|
||||
G_N_ELEMENTS(keyboard_focus));
|
||||
const char *echo_toggles[] = {"keyEchoCheckButton", "useCustomEchoVoiceCheckButton", "useCustomEchoSpeechServerCheckButton", "enableEchoByCharacterCheckButton", "enableEchoByWordCheckButton", "enableEchoBySentenceCheckButton"};
|
||||
for (unsigned i = 0; i < G_N_ELEMENTS(echo_toggles); i++) g_signal_connect(prefs_widget(echo_toggles[i]), "toggled", G_CALLBACK(echo_sensitivity), NULL);
|
||||
echo_sensitivity(NULL, NULL);
|
||||
g_signal_connect(prefs_widget("enableSoundCheckButton"), "toggled", G_CALLBACK(sound_sensitivity), NULL);
|
||||
sound_sensitivity(NULL, NULL);
|
||||
g_signal_connect(prefs_widget("ocrBlackWhiteCheckButton"), "toggled", G_CALLBACK(ocr_sensitivity), NULL);
|
||||
g_signal_connect(prefs_widget("ocrAutoRefreshCheckButton"), "toggled", G_CALLBACK(ocr_sensitivity), NULL);
|
||||
ocr_sensitivity(NULL, NULL);
|
||||
gtk_widget_show_all(prefs_widget("window"));
|
||||
for (unsigned int i = 0; i < G_N_ELEMENTS(named); i++) {
|
||||
GtkWidget *control = prefs_widget(named[i].id);
|
||||
if (GTK_IS_COMBO_BOX_TEXT(control)) {
|
||||
combo_changed(GTK_COMBO_BOX(control), (gpointer)named[i].name);
|
||||
}
|
||||
}
|
||||
gtk_widget_grab_focus(prefs_widget("notebook"));
|
||||
return 1;
|
||||
}
|
||||
void prefs_set_application(const char *name) {
|
||||
if (!name || !name[0]) return;
|
||||
// Hiding a notebook child leaves its tab in the AT-SPI tree. Remove the
|
||||
// page entirely so application preferences expose no global controls.
|
||||
GtkNotebook *notebook = GTK_NOTEBOOK(prefs_widget("notebook"));
|
||||
gtk_notebook_remove_page(notebook, gtk_notebook_page_num(notebook, prefs_widget("tentacles_page")));
|
||||
gchar *title = g_strdup_printf("Cthulhu Preferences for %s", name);
|
||||
gtk_window_set_title(GTK_WINDOW(prefs_widget("window")), title);
|
||||
g_free(title);
|
||||
}
|
||||
void prefs_iterate(void) { for (int i = 0; i < 64 && g_main_context_pending(NULL); i++) g_main_context_iteration(NULL, FALSE); }
|
||||
int prefs_action(void) { int result = action; action = 0; return result; }
|
||||
int prefs_voice_changed(void) { int result = voice_changed; voice_changed = 0; return result; }
|
||||
void prefs_busy(int busy, int can_save) {
|
||||
gtk_widget_set_sensitive(prefs_widget("notebook"), !busy);
|
||||
gtk_widget_set_sensitive(prefs_widget("apply"), !busy);
|
||||
gtk_widget_set_sensitive(prefs_widget("save"), !busy && can_save);
|
||||
if (!busy && busy_focus) {
|
||||
if (gtk_widget_get_sensitive(busy_focus)) gtk_widget_grab_focus(busy_focus);
|
||||
busy_focus = NULL;
|
||||
}
|
||||
}
|
||||
void prefs_present(void) { gtk_window_present(GTK_WINDOW(prefs_widget("window"))); }
|
||||
void prefs_close(void) { busy_focus = NULL; gtk_widget_destroy(prefs_widget("window")); g_object_unref(builder); builder = NULL; }
|
||||
void prefs_status(const char *message, int focus) {
|
||||
gtk_entry_set_text(GTK_ENTRY(prefs_widget("status")), message);
|
||||
atk_object_set_name(gtk_widget_get_accessible(prefs_widget("status")), message);
|
||||
if (focus) gtk_widget_grab_focus(prefs_widget("status"));
|
||||
}
|
||||
Reference in new issue
Block a user