Merge branch 'deedra'

This commit is contained in:
Storm Dragon committed 2026-10-14 20:13:11 -04:00
commit 4c47cc5fc4
5 files changed
+112 -12

No files matched your search

+4
View File
@@ -244,6 +244,10 @@ It reports accepted typed characters, the current line on focus and menu return,
characters and words on horizontal navigation, lines on vertical/page/document-boundary
navigation, changed selections, and Backspace/Delete/Cut feedback. Password
contents are excluded, and read-only edits do not report unsuccessful deletions.
Command-history recalls in MUSHclient report the recalled command, or `blank`
when returning to empty input. The input hook observes Up/Down before the
application consumes them, so recalls also work when the caret position stays
the same. Unrelated programmatic text changes remain silent.
Undo refreshes the text snapshot used by subsequent navigation. It can attach to
an already-focused editor and resume after the helper restarts.
+29 -1
View File
@@ -15,6 +15,7 @@ typedef struct EditState {
WCHAR *text;
DWORD length;
BOOL busy, known, destroyed;
BOOL history_key, history_replaced;
WCHAR pending_high;
DWORD pending_caret;
} EditState;
@@ -102,6 +103,10 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
BOOL requested_focus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE);
BOOL focus = message == WM_SETFOCUS || requested_focus;
BOOL selected = message == EM_SETSEL;
// A dispatched arrow belongs to ordinary caret navigation. Applications
// such as MUSHclient consume history arrows before DispatchMessage instead.
if (navigation || focus) state->history_key = state->history_replaced = FALSE;
BOOL history_replacement = message == WM_SETTEXT && state->history_key;
if (!navigation && !changing && !focus && !selected && message != EM_SETPASSWORDCHAR)
return DefSubclassProc(window, message, wp, lp);
@@ -152,7 +157,17 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
if (focus) {
current_line(window, start, text);
kind = WINE_ACCESS_TEXT_FOCUS; report = TRUE;
} else if ((navigation || selected) && (start != old_start || end != old_end)) {
} else if (history_replacement && result) {
// Pretranslated Up/Down recalls arrive as SetWindowText followed by
// SetSel, without any WM_KEYDOWN reaching this procedure. Read the whole
// recalled command once, including blank and equal-length replacements.
if (state->text) copy_range(text, state->text, 0, state->length);
else current_line(window, start, text);
state->history_key = FALSE;
state->history_replaced = TRUE;
report = TRUE;
} else if ((navigation || (selected && !state->history_replaced)) &&
(start != old_start || end != old_end)) {
if (start != end && state->text && end <= state->length) {
// Speak the changed portion when extending or shrinking a selection.
DWORD first = start, last = end;
@@ -262,6 +277,19 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp,
HWND focus = GetFocus();
if (wine_access_watch_edit(focus))
SendMessageW(focus, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
EditState *state = (EditState *)GetPropW(focus, EDIT_STATE_PROPERTY);
WCHAR class_name[64];
// Scintilla uses the same attachment property with a different state.
if (state && GetClassNameW(focus, class_name, 64) &&
wine_access_class_matches(class_name, L"Edit") && !state->busy) {
const MSG *message = (const MSG *)lp;
// Restrict replacement feedback to this retrieved input message.
// Timers, unrelated programmatic changes and background edits stay silent.
state->history_key = message->hwnd == focus && message->message == WM_KEYDOWN &&
(message->wParam == VK_UP || message->wParam == VK_DOWN) &&
!(GetKeyState(VK_CONTROL) & 0x8000);
state->history_replaced = FALSE;
}
#ifdef _WIN64
wine_access_scintilla_input(focus, (const MSG *)lp);
#endif
+17 -10
View File
@@ -304,9 +304,13 @@ Ctrl+End line reading, and Up/Down navigation in a read-only multiline output
control. Typing checks include repeated letters, spaces, Enter/Tab, accented
characters, surrogate pairs, selection replacement, documents beyond the full
snapshot limit, and silence for password/background/read-only fields and
input rejected at the text limit. Both 32-bit and 64-bit fixtures use the same
assertions. Lifecycle variants attach after the control has focus or
restart the helper without closing it. Every text action must have an established
input rejected at the text limit. Command-history checks consume queued Up/Down
before dispatch, then replace the text and move the caret as MUSHclient does.
They require ordered, single announcements for equal-length and rapid recalls
and empty input, with no password, background or unrelated programmatic output.
Both 32-bit and 64-bit fixtures use the same assertions. Lifecycle variants
attach after the control has focus or restart the helper without closing it.
Every text action must have an established
focus source, matching the Go reader's filtering contract.
The Notepad fixture launches Wine's actual `notepad.exe` and uses `SendInput`
@@ -315,13 +319,16 @@ It checks resulting document text as well as recorded accessibility output. Thes
tests use a recording D-Bus service; they do not run the full reader or prove
audible typing echo, physical keys, or a physical braille display.
For live MUSHclient acceptance, install the helper and reopen MUSHclient so the
updated 32-bit DLL can load. In the command field, check Left/Right and
Ctrl+Left/Right feedback without submitting a command. Use Ctrl+Tab to reach
the read-only output notepad supplied by the Aardwolf review-buffer plugin;
check Up/Down line reading and Left/Right characters there, then return to
the command field. Check plugin menus and dialogs still work, and repeat after
restarting the reader. These hooks provide caret feedback; independent review
For live MUSHclient acceptance, install the helper, restart Cthulhu and reopen
MUSHclient so the updated 32-bit DLL can load. In the command field, check Left/Right and
Ctrl+Left/Right feedback without submitting a command. Use Up/Down to recall
existing command history. Check each recalled command is spoken once,
including equal-length entries and `blank` when returning to empty input.
Do not submit recalled commands just to test speech.
Use Ctrl+Tab to reach the read-only output notepad supplied by the Aardwolf
review-buffer plugin; check Up/Down line reading and Left/Right characters
there, then return to the command field. Check plugin menus and dialogs still
work, and repeat after restarting the reader. These hooks provide caret feedback; independent review
commands use the optional flat-review protocol described above. Native braille
routing into Wine text remains unsupported.
+55 -1
View File
@@ -18,8 +18,24 @@ static void register_control(HINSTANCE instance, const WCHAR *nativeName, const
static const WCHAR *editClass = L"Edit";
#endif
static HWND editor, password;
static HWND editor, password, history;
static int step;
static int history_position;
// Model MUSHclient's PreTranslateMessage: consume queued arrows before the
// Edit procedure, then use SetWindowText and SetSel to recall a command.
static BOOL history_input(const MSG *message) {
if (message->hwnd != history || message->message != WM_KEYDOWN ||
(message->wParam != VK_UP && message->wParam != VK_DOWN)) return FALSE;
if (message->wParam == VK_UP && history_position < 2) ++history_position;
else if (message->wParam == VK_DOWN && history_position > 0) --history_position;
else return TRUE;
const WCHAR *commands[] = {L"", L"history north", L"history south"};
SetWindowTextW(history, commands[history_position]);
int length = GetWindowTextLengthW(history);
SendMessageW(history, EM_SETSEL, length, length);
return TRUE;
}
static void key(UINT key, BOOL control, BOOL shift) {
BYTE saved[256], state[256];
@@ -194,6 +210,43 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
SendMessageW(editor, WM_CHAR, L'%', 1);
if (GetWindowTextLengthW(editor) != 4) ExitProcess(2);
break;
case 41:
// Keep this field away from the managed-label regression's static labels.
history = CreateWindowW(editClass, L"", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
100, 150, 300, 25, window, NULL, GetModuleHandleW(NULL), NULL);
SetFocus(history);
break;
case 42: PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break;
case 43: PostMessageW(history, WM_KEYDOWN, VK_UP, 1); break;
case 44: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break;
case 45: PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); break;
case 46:
// Rapid recalls must stay ordered, including equal-length commands.
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1);
break;
case 47:
// Ordinary navigation after a recall must use the refreshed snapshot.
PostMessageW(history, WM_KEYDOWN, VK_LEFT, 1);
PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1);
break;
case 48:
SetWindowTextW(history, L"programmatic history sentinel");
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1);
break;
case 49:
PostMessageW(history, WM_KEYDOWN, VK_DOWN, 1); // no history change
break;
case 50:
SendMessageW(history, EM_SETPASSWORDCHAR, L'*', 0);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
break;
case 51:
SetFocus(password);
SendMessageW(history, EM_SETPASSWORDCHAR, 0, 0);
PostMessageW(history, WM_KEYDOWN, VK_UP, 1); // background recall
break;
default:
DestroyWindow(window); break;
}
@@ -245,6 +298,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
SetTimer(window, 1, 1500, NULL);
MSG message;
while (GetMessageW(&message, NULL, 0, 0) > 0) {
if (history_input(&message)) continue;
TranslateMessage(&message); DispatchMessageW(&message);
}
return 0;
+7
View File
@@ -312,6 +312,13 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
typed = [e[2] for e in text_events if e[1] == "text" and "kind=8" in e[5]]
assert typed == ["z", "z", "space", "\u00e9", "\U0001f600", "new line", "tab", "q", "L", "\U0001f600"], f"typing echo missing or incorrect: {typed!r}"
assert not any(e[1] == "text" and e[2] in ("!", "@", "~", "%") for e in events), "rejected, password or background typing leaked"
history_focus = next(e[0] for e in text_events
if e[1] == "focus" and e[4] == "blank")
history_actions = [e[2] for e in text_events
if e[0] == history_focus and e[1] == "text"]
assert history_actions == ["history north", "history south", "history north", "blank",
"history north", "history south", "history north", "h", "blank", "blank"], \
f"command history missing, reordered, duplicated or leaked: {history_actions!r}"
if args.fixture == "winforms":
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
for e in text_events), "managed email label/focus missing"