Add a description column to the reading settings table under speech.

This commit is contained in:
Storm Dragon committed 2026-10-20 19:33:48 -04:00
1 parent ffcea2efea
commit 41283a5d8d
7 files changed
+219 -46

No files matched your search

+14 -5
View File
@@ -64,6 +64,7 @@ func flatReviewCommand(command input.Command) (review.FlatUnit, int) {
func presentFlatReviewResult(logger *slog.Logger, speech speechEnqueuer, result review.Result) {
text := ""
boundary := false
if result.Err != nil {
logger.Warn("flat review failed", "error", result.Err)
switch {
@@ -75,16 +76,24 @@ func presentFlatReviewResult(logger *slog.Logger, speech speechEnqueuer, result
text = "Could not find current location."
}
} else if !result.Moved && (result.FlatStep != 0 || result.FlatUnit == review.WordAbove || result.FlatUnit == review.WordBelow) {
// Python's sequential review stops silently at window boundaries.
return
if result.FlatUnit != review.Line {
return
}
boundary = true
if result.FlatStep < 0 {
text = "Top of screen."
} else {
text = "Bottom of screen."
}
} else {
text = presentation.FlatReviewText(result.FlatPosition.Text, result.FlatUnit == review.Character || result.FlatUnit == review.LineEnd)
}
request := speechRequest{Text: text, Role: "text", Cause: "flat-review", Character: result.Err == nil && (result.FlatUnit == review.Character || result.FlatUnit == review.LineEnd)}
if result.Err == nil && result.FlatUnit == review.Line {
hasContent := result.Err == nil && !boundary
request := speechRequest{Text: text, Role: "text", Cause: "flat-review", Character: hasContent && (result.FlatUnit == review.Character || result.FlatUnit == review.LineEnd)}
if hasContent && result.FlatUnit == review.Line {
request.Line = &speechLine{Object: result.FlatPosition.Object, Raw: result.FlatPosition.Text}
}
if result.Err == nil {
if hasContent {
set_review_spelling(&request, result.FlatPosition.Text, result.FlatSpeech)
}
speech.Enqueue(request)
+45 -1
View File
@@ -76,6 +76,14 @@ func TestProcessEventsPresentsFlatReviewThroughSpeechQueue(t *testing.T) {
expect("Review field, text")
worker.ReadFlat(review.Line, 0)
expect("a 猫!oo")
worker.ReadFlat(review.Line, -1)
expect("Top of screen.")
worker.ReadFlat(review.Line, 0)
expect("a 猫!oo")
worker.ReadFlat(review.Line, 1)
expect("Bottom of screen.")
worker.ReadFlat(review.Line, 0)
expect("a 猫!oo")
worker.ReadFlat(review.Character, 1)
expect("space")
worker.ReadFlat(review.Character, 1)
@@ -122,11 +130,47 @@ func TestFlatReviewResultUsesNativeFocusAndRejectsOldPage(t *testing.T) {
}
}
func TestFlatReviewLineBoundaryFeedback(t *testing.T) {
for _, tc := range []struct {
name string
step int
want string
}{
{"top", -1, "Top of screen."},
{"bottom", 1, "Bottom of screen."},
} {
t.Run(tc.name, func(t *testing.T) {
queue := &autocompleteRequestQueue{requests: make(chan speechRequest, 1)}
result := review.Result{
FlatUnit: review.Line, FlatStep: tc.step, FlatSpeech: review.FlatSpell,
FlatPosition: review.FlatPosition{Text: " Edge line"},
}
for range 2 {
presentFlatReviewResult(slog.New(slog.DiscardHandler), queue, result)
select {
case request := <-queue.requests:
if request.Text != tc.want || request.Cause != "flat-review" {
t.Fatalf("boundary request = %+v", request)
}
if request.Line != nil || request.Character || len(request.Spelling) != 0 {
t.Fatalf("boundary retained content speech metadata: %+v", request)
}
default:
t.Fatal("boundary was silent")
}
}
})
}
}
func TestFlatReviewErrorsAndSilentBoundary(t *testing.T) {
queue := &fakeSpeechQueue{spoken: make(chan string, 2)}
logger := slog.New(slog.DiscardHandler)
for _, result := range []review.Result{
{FlatUnit: review.Line, FlatStep: 1},
{FlatUnit: review.Word, FlatStep: -1},
{FlatUnit: review.Word, FlatStep: 1},
{FlatUnit: review.Character, FlatStep: -1},
{FlatUnit: review.Character, FlatStep: 1},
{FlatUnit: review.WordAbove},
{FlatUnit: review.WordBelow},
} {
+11 -3
View File
@@ -79,17 +79,25 @@ func TestGUITableNormalizedFocusRetainsOwnerAndTitle(t *testing.T) {
other.Source.BusName = ":1.99"
adapter.events <- other
expect_speech(second, 6, "checked, Break speech into chunks between pauses")
// A third description cell participates in full-row speech, while
// horizontal movement reads just that cell.
description := "Reads numbers one digit at a time."
rowText := "unchecked, Speak numbers as digits, " + description
third := cell_event("/reading/2/state", "unchecked", rowText, 2, 0, atspi.SelectionChangedEvent)
expect_speech(third, 7, rowText)
descriptionCell := cell_event("/reading/2/description", description, rowText, 2, 2, atspi.ActiveDescendantEvent)
expect_speech(descriptionCell, 8, description)
button := atspi.NewSnapshot("/preferences/apply", "Apply", "button", "", "")
button.BusName = window.BusName
focus := atspi.NewEvent(atspi.FocusEvent, button)
focus.Window = window
expect_speech(focus, 7, "Apply, button")
expect_speech(focus, 9, "Apply, button")
adapter.events <- up // A table selection after leaving the table is noise.
// Returning through a cell focus event establishes ownership and speaks
// the table name again, without changing the user's row-reading option.
first.OriginPath = first.Source.Path
expect_speech(first, 8, "Reading settings, unchecked, Only speak displayed text")
expect_speech(second, 9, "checked, Break speech into chunks between pauses")
expect_speech(first, 10, "Reading settings, unchecked, Only speak displayed text")
expect_speech(second, 11, "checked, Break speech into chunks between pauses")
cancel()
<-completed
}
+76 -35
View File
@@ -4,46 +4,80 @@
#include "native_internal.h"
static GtkListStore *reading_store;
static const struct { const char *id; const char *label; } reading_rows[] = {
{"only_speak_displayed_text", "Only speak displayed text"},
{"automatically_read_title_changes", "Automatically read title changes"},
{"automatically_read_clipboard_changes", "Automatically read clipboard changes"},
{"phonetic_character_hints", "Phonetic character hints"},
{"enable_pause_breaks", "Break speech into chunks between pauses"},
{"speak_numbers_as_digits", "Speak numbers as digits"},
{"speak_multicase_strings_as_words", "Speak multicase strings as words"},
{"enable_indentation", "Enable indentation presentation"},
{"speak_blank_lines", "Speak blank lines"},
{"enable_mnemonic_speaking", "Speak object mnemonics"},
{"enable_position_speaking", "Speak child position"},
{"enable_tutorial_messages", "Speak tutorial messages"},
{"speak_description", "Speak object descriptions"},
{"messages_are_detailed", "System messages are detailed"},
{"use_color_names", "Speak colors as names"},
{"speak_misspelled_indicator", "Speak misspelled-word indicator"},
{"speak_context_blockquote", "Announce block quotes during navigation"},
{"speak_context_list", "Announce lists during navigation"},
{"speak_context_table", "Announce tables during navigation"},
{"speak_context_panel", "Announce panels during navigation"},
{"speak_context_landmark", "Announce landmarks during navigation"},
{"speak_context_non_landmark_form", "Announce forms during navigation"},
{"read_full_row_in_gui_table", "Speak full row in GUI tables"},
{"read_full_row_in_document_table", "Speak full row in document tables"},
{"read_full_row_in_spreadsheet", "Speak full row in spreadsheets"},
{"present_tooltips", "Present tooltips"},
{"enable_mouse_review", "Speak object under mouse"},
{"say_all_context_blockquote", "Announce block quotes in Say All"},
{"say_all_context_list", "Announce lists in Say All"},
{"say_all_context_table", "Announce tables in Say All"},
{"say_all_context_panel", "Announce panels in Say All"},
{"say_all_context_non_landmark_form", "Announce forms in Say All"},
{"say_all_context_landmark", "Announce landmarks in Say All"},
static const struct { const char *id; const char *label; const char *description; } reading_rows[] = {
{"only_speak_displayed_text", "Only speak displayed text",
"Limits reading to displayed names, text and values. Omits extra information such as control types, states and container announcements."},
{"automatically_read_title_changes", "Automatically read title changes",
"Announces changes to the active window's title, such as a new page title in a browser."},
{"automatically_read_clipboard_changes", "Automatically read clipboard changes",
"Automatically reads new text when the clipboard contents change, such as after copying text."},
{"phonetic_character_hints", "Phonetic character hints",
"When navigating characters with Left or Right, adds a phonetic name after a 300 ms pause, for example, a then alpha. Further input cancels the hint."},
{"enable_pause_breaks", "Break speech into chunks between pauses",
"Separates parts of an announcement with pauses, such as between a control's name, type and state."},
{"speak_numbers_as_digits", "Speak numbers as digits",
"Reads numbers one digit at a time, for example, 123 as one two three."},
{"speak_multicase_strings_as_words", "Speak multicase strings as words",
"Separates words where a lowercase letter is followed by an uppercase letter, for example, saveFile as save File."},
{"enable_indentation", "Enable indentation presentation",
"Reports changes in a line's leading spaces and tabs using the selected indentation presentation mode."},
{"speak_blank_lines", "Speak blank lines",
"Says blank when navigating to an empty line, instead of leaving it silent."},
{"enable_mnemonic_speaking", "Speak object mnemonics",
"Announces a control's access key when the application provides one, such as Alt+O for an Open button."},
{"enable_position_speaking", "Speak child position",
"Announces an item's position within a group when available, for example, 2 of 5."},
{"enable_tutorial_messages", "Speak tutorial messages",
"Adds brief instructions for using the focused control, such as Press Space to toggle for a checkbox."},
{"speak_description", "Speak object descriptions",
"Reads additional descriptive text supplied by the application for a control or window."},
{"messages_are_detailed", "System messages are detailed",
"Reads a notification's full message when available. When disabled, reads just its summary when one is provided."},
{"use_color_names", "Speak colors as names",
"Uses color names when reporting text colors, instead of their numeric color values."},
{"speak_misspelled_indicator", "Speak misspelled-word indicator",
"Says misspelled when reading text that the application's spell checker has marked as incorrect."},
{"speak_context_blockquote", "Announce block quotes during navigation",
"Announces when you enter or leave a block quote while navigating text."},
{"speak_context_list", "Announce lists during navigation",
"Announces when you enter or leave a list while navigating."},
{"speak_context_table", "Announce tables during navigation",
"Announces when you enter or leave a data table while navigating."},
{"speak_context_panel", "Announce panels during navigation",
"Announces when you enter or leave a panel or group of controls while navigating."},
{"speak_context_landmark", "Announce landmarks during navigation",
"Announces when you enter or leave a landmark, such as a page's main content or navigation region."},
{"speak_context_non_landmark_form", "Announce forms during navigation",
"Announces when you enter or leave a form that is not identified as a landmark."},
{"read_full_row_in_gui_table", "Speak full row in GUI tables",
"Reads all cells in a row when moving between rows in application tables, including this settings table. Left and Right read individual cells."},
{"read_full_row_in_document_table", "Speak full row in document tables",
"Reads the whole row when navigating rows in a document or web page table. Moving between columns reads individual cells."},
{"read_full_row_in_spreadsheet", "Speak full row in spreadsheets",
"Reads the whole row when navigating rows in a spreadsheet. Moving between columns reads individual cells."},
{"present_tooltips", "Present tooltips",
"Reads tooltip text when the application displays it."},
{"enable_mouse_review", "Speak object under mouse",
"Reads the object under the mouse pointer as you move the mouse."},
{"say_all_context_blockquote", "Announce block quotes in Say All",
"Announces entry into and exit from block quotes during continuous reading with Say All. This is separate from navigation announcements."},
{"say_all_context_list", "Announce lists in Say All",
"Announces entry into and exit from lists during continuous reading with Say All. This is separate from navigation announcements."},
{"say_all_context_table", "Announce tables in Say All",
"Announces entry into and exit from data tables during continuous reading with Say All. This is separate from navigation announcements."},
{"say_all_context_panel", "Announce panels in Say All",
"Announces entry into and exit from panels or groups of controls during continuous reading with Say All. This is separate from navigation announcements."},
{"say_all_context_non_landmark_form", "Announce forms in Say All",
"Announces entry into and exit from forms that are not landmarks during continuous reading with Say All. This is separate from navigation announcements."},
{"say_all_context_landmark", "Announce landmarks in Say All",
"Announces entry into and exit from landmarks, such as main content or navigation regions, during continuous reading with Say All. This is separate from navigation announcements."},
};
enum {
READING_ID,
READING_LABEL,
READING_ENABLED,
READING_DESCRIPTION,
READING_N_COLUMNS
};
static void name_reading_toggle_cells(void);
@@ -94,7 +128,7 @@ static void name_reading_toggle_cells(void) {
}
void prefs_init_reading_settings(void) {
reading_store = gtk_list_store_new(READING_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
reading_store = gtk_list_store_new(READING_N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING);
for (unsigned int i = 0; i < G_N_ELEMENTS(reading_rows); i++) {
GtkTreeIter iter;
gtk_list_store_append(reading_store, &iter);
@@ -102,6 +136,7 @@ void prefs_init_reading_settings(void) {
READING_ID, reading_rows[i].id,
READING_LABEL, reading_rows[i].label,
READING_ENABLED, FALSE,
READING_DESCRIPTION, reading_rows[i].description,
-1);
}
@@ -118,6 +153,12 @@ void prefs_init_reading_settings(void) {
"Enabled", toggle_renderer, "active", READING_ENABLED, NULL);
gtk_tree_view_append_column(view, enabled_column);
gtk_tree_view_append_column(view, setting_column);
GtkCellRenderer *descriptionRenderer = gtk_cell_renderer_text_new();
g_object_set(descriptionRenderer, "wrap-mode", PANGO_WRAP_WORD_CHAR, "wrap-width", 280, NULL);
GtkTreeViewColumn *descriptionColumn = gtk_tree_view_column_new_with_attributes(
"Description", descriptionRenderer, "text", READING_DESCRIPTION, NULL);
gtk_tree_view_column_set_expand(descriptionColumn, TRUE);
gtk_tree_view_append_column(view, descriptionColumn);
g_signal_connect(view, "key-press-event", G_CALLBACK(reading_key_press), NULL);
name_reading_toggle_cells();
}