Cthulhu is no longer a fork. Now it is a screen reader written in Golang.

This commit is contained in:
Storm Dragon committed 2026-10-14 00:16:34 -04:00
1 parent a998fe67de
commit 069fc674dc
708 files changed
+112482 -532

No files matched your search

+280
View File
@@ -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"));
}