Echo typed characters in Wine edit fields

This commit is contained in:
deedra committed 2026-10-12 15:31:00 -07:00
1 parent d9aa40e16c
commit a002c4cc1e
7 files changed
+154 -22

No files matched your search

+15 -8
View File
@@ -193,8 +193,8 @@ document area, MUSHclient's command field and read-only output notepad, and
use visible sibling labels immediately to their left, even when the labels
were created after the fields. Protected fields report their label and
password role without forwarding their contents.
It reports the current line on focus and menu return, characters
and words on horizontal navigation, lines on vertical/page/document-boundary
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.
Undo refreshes the text snapshot used by subsequent navigation. It can attach to
@@ -212,11 +212,18 @@ selection feedback and Ctrl+Backspace/Delete feedback are not implemented.
Backspace reports only the removed text, such as `t`. Delete and Cut retain
their deletion feedback, such as `t deleted`.
Standard Edit typing sends each accepted character as an ordered editor action,
including repeated letters, spaces, tabs, line breaks and Unicode surrogate
pairs. Typing over selected text also reports the inserted character. Password
fields, background controls and rejected input remain silent. Paste and
programmatic text changes do not generate character echo. The reader presents
these through the same feedback path as caret navigation and Backspace.
Editor actions use `PresentAccessibleEvent` with event type `text`; actual focus
uses `focus`. Cthulhu-Go must include the corresponding editor-event support so
rapid actions and repeated characters bypass focus coalescing. Selection
offsets and the action kind travel in the otherwise non-spoken `states` field
for debug diagnostics. Standard edits use UTF-16 offsets; Scintilla marks its
offsets and the action kind (8 for typed insertion) travel in the otherwise
non-spoken `states` field for debug diagnostics. Standard edits use UTF-16 offsets; Scintilla marks its
encoded byte offsets with `offset_unit=byte`. Older readers that only accept
focus/value/selection events still receive focus and menus, but need updating
for editor actions.
@@ -262,10 +269,10 @@ label captions decode keyboard mnemonics and escaped `&&`; static labels with
These hooks provide editing feedback. Independent review-cursor commands use
the optional flat-review protocol described below; Say All and native braille
routing remain unsupported. Typing echo
remains the reader's responsibility. RichEdit and 32-bit Scintilla hooks are not
covered. Reports are limited to 1023 UTF-16 units; documents over one million
units use line feedback without full selection/deletion snapshots.
routing remain unsupported. Word echo and typing echo in Scintilla
remain outside this helper's current support. RichEdit and 32-bit Scintilla hooks
are not covered. Reports are limited to 1023 UTF-16 units; documents over one million
units retain typing and line feedback without full selection/deletion snapshots.
Scintilla reads at most 4096 encoded bytes per report, then limits output to
1023 UTF-16 units without splitting surrogate pairs. Wrapped Scintilla lines
report the underlying document line.
+36
View File
@@ -14,6 +14,8 @@ typedef struct EditState {
WCHAR *text;
DWORD length;
BOOL busy, known, destroyed;
WCHAR pending_high;
DWORD pending_caret;
} EditState;
static void selection(HWND window, DWORD *start, DWORD *end) {
@@ -94,6 +96,8 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
message == WM_PASTE || message == WM_CUT || message == WM_CLEAR ||
message == WM_UNDO || message == EM_UNDO || message == EM_REPLACESEL ||
message == WM_SETTEXT || message == EM_SETHANDLE || message == EM_FMTLINES;
BOOL typing = message == WM_CHAR &&
((wp >= 0x20 && wp <= 0xffff && wp != 0x7f) || wp == VK_RETURN || wp == VK_TAB);
BOOL requested_focus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE);
BOOL focus = message == WM_SETFOCUS || requested_focus;
BOOL selected = message == EM_SETSEL;
@@ -101,6 +105,9 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
return DefSubclassProc(window, message, wp, lp);
state->busy = TRUE;
WCHAR pending_high = state->pending_high;
DWORD pending_caret = state->pending_caret;
state->pending_high = 0;
DWORD old_start = 0, old_end = 0;
selection(window, &old_start, &old_end);
BOOL password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0;
@@ -185,6 +192,35 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
kind = message == WM_CHAR && wp == VK_BACK
? WINE_ACCESS_TEXT_BACKSPACED : WINE_ACCESS_TEXT_DELETED;
report = TRUE;
} else if (typing && !(GetWindowLongPtrW(window, GWL_STYLE) & ES_READONLY) &&
start == end && start > old_start && old_start <= old_end &&
old_end <= old_length && state->length > old_length - (old_end - old_start)) {
// Echo accepted input, including replacements that shorten a selection.
// WM_CHAR delivers supplementary characters as two UTF-16 messages.
if (wp >= 0xd800 && wp <= 0xdbff) {
state->pending_high = (WCHAR)wp;
state->pending_caret = start;
} else if (wp < 0xdc00 || wp > 0xdfff ||
(pending_high && pending_caret == old_start && old_start == old_end)) {
DWORD first = old_start;
BOOL paired = wp >= 0xdc00 && wp <= 0xdfff;
if (paired) --first;
if (state->text && start <= state->length) {
copy_range(text, state->text, first, start);
} else {
// Large documents have no full snapshot. The accepted WM_CHAR still
// supplies bounded typing feedback without copying the document.
UINT used = 0;
if (paired) text[used++] = pending_high;
text[used++] = (WCHAR)wp;
text[used] = 0;
LONG_PTR style = GetWindowLongPtrW(window, GWL_STYLE);
if (style & ES_UPPERCASE) CharUpperBuffW(text, used);
else if (style & ES_LOWERCASE) CharLowerBuffW(text, used);
}
kind = WINE_ACCESS_TEXT_INSERTED;
report = text[0] != 0;
}
}
if (report) notify_text(window, kind, text, start, end);
state->busy = FALSE;
+1
View File
@@ -17,6 +17,7 @@
#define WINE_ACCESS_TEXT_READ 5
#define WINE_ACCESS_TEXT_PASSWORD 6
#define WINE_ACCESS_TEXT_BACKSPACED 7
#define WINE_ACCESS_TEXT_INSERTED 8
#define WINE_ACCESS_TEXT_SCINTILLA 1
#define WINE_ACCESS_TEXT_READONLY 2
+17 -11
View File
@@ -193,21 +193,24 @@ python3 tests/menu_integration.py --build-dir build --fixture winforms --32-bit
python3 tests/menu_integration.py --build-dir build --fixture notepad
```
The standard Edit fixture checks focus, characters/words/lines, incremental
selection and unselection, Backspace/Delete/Clear, read-only controls, password
The standard Edit fixture checks focus, typing echo, characters/words/lines,
incremental selection and unselection, Backspace/Delete/Clear, read-only controls, password
exclusion, surrogate pairs, rapid repeated letters, menu return at the caret,
short replacements, CRLF deletion, long-line truncation, Undo snapshots, and
Ctrl+End line reading, and Up/Down navigation in a read-only multiline output
control. Both 32-bit and 64-bit fixtures use the same assertions. Lifecycle
variants attach after the control has focus or
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
focus source, matching the Go reader's filtering contract.
The Notepad fixture launches Wine's actual `notepad.exe` and uses `SendInput`
for arrows, Ctrl+Right, Shift selection, Delete, and Alt/Escape menu return. It
checks resulting document text as well as recorded accessibility output. These
for typing, arrows, Ctrl+Right, Shift selection, Delete, and Alt/Escape menu return.
It checks resulting document text as well as recorded accessibility output. These
tests use a recording D-Bus service; they do not run the full reader or prove
audible speech, typing echo, physical keys, or a physical braille display.
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
@@ -408,8 +411,9 @@ helper and the application load the new DLL together. Reopen BARD in its own
prefix. Tab and Shift+Tab between Email, Password, Sign in and Work Offline.
Expect labels and edit/password roles; navigate and select ordinary email
text, and confirm password contents stay excluded. Recheck native Notepad,
menus, the installer list and switching to Linux applications. Typing echo
and routable Wine review remain separate unfinished features.
menus, the installer list and switching to Linux applications. Standard Edit
typing echo is now forwarded through editor events; routable Wine review
remains unfinished.
## Live Notepad acceptance
@@ -421,8 +425,10 @@ Close existing Notepad windows after saving any work so the updated hook can loa
rebuilds Cthulhu and its braille helper and opens the test terminal; it does
not build wine-access.
3. Run `wine notepad` in the intended Wine prefix. Expect edit/blank focus
feedback. Type `hello beta`, Enter, then `second line`. Typing echo should
follow the reader's configured settings.
feedback. Type `hello beta`, Enter, then `second line`. Expect each typed
character, including both `l` characters, spaces and the line break. Standard
Edit typing uses the reader's editor feedback path. Check typing over selected
text too, and confirm password fields never announce their typed contents.
4. Press Ctrl+Home, then Right three times. Expect the first line at Ctrl+Home,
then `e`, `l`, `l`. Both repeated letters should produce feedback. Ctrl+Right
should read `beta`; Down should read `second line`.
+63
View File
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef WINE_ACCESS_WINFORMS_FIXTURE
static const WCHAR *editClass = L"WindowsForms10.EDIT.app.0.fixture_r7_ad1";
@@ -131,6 +133,67 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
key(VK_DELETE, FALSE, FALSE);
if (GetWindowTextLengthW(editor) != 31) ExitProcess(2);
break;
case 35:
SendMessageW(editor, WM_CHAR, L'!', 1);
if (GetWindowTextLengthW(editor) != 31) ExitProcess(2);
break;
case 36:
SendMessageW(editor, EM_SETREADONLY, FALSE, 0);
SetWindowTextW(editor, L"");
SendMessageW(editor, WM_CHAR, L'z', 1);
SendMessageW(editor, WM_CHAR, L'z', 1);
SendMessageW(editor, WM_CHAR, L' ', 1);
SendMessageW(editor, WM_CHAR, 0x00e9, 1);
SendMessageW(editor, WM_CHAR, 0xd83d, 1);
SendMessageW(editor, WM_CHAR, 0xde00, 1);
SendMessageW(editor, WM_CHAR, VK_RETURN, 1);
SendMessageW(editor, WM_CHAR, VK_TAB, 1);
{
WCHAR text[32];
GetWindowTextW(editor, text, 32);
if (lstrcmpW(text, L"zz \u00e9\U0001f600\r\n\t")) ExitProcess(2);
}
break;
case 37:
// Replacing selected text is still typing, even when length shrinks.
SendMessageW(editor, EM_SETSEL, 0, 4);
SendMessageW(editor, WM_CHAR, L'q', 1);
break;
case 38:
SetFocus(password);
SendMessageW(password, WM_CHAR, L'@', 1);
// Background changes must not produce echo for the focused password.
SendMessageW(editor, WM_CHAR, L'~', 1);
break;
case 39:
SetFocus(editor);
{
DWORD length = 1024 * 1024 + 1;
WCHAR *large = calloc(length + 1, sizeof(WCHAR));
if (!large) ExitProcess(2);
for (DWORD i = 0; i < length; ++i) large[i] = L'x';
SendMessageW(editor, EM_SETLIMITTEXT, length + 3, 0);
SetWindowTextW(editor, large);
free(large);
SendMessageW(editor, EM_SETSEL, length, length);
SendMessageW(editor, WM_CHAR, L'L', 1);
SendMessageW(editor, WM_CHAR, 0xd83d, 1);
SendMessageW(editor, WM_CHAR, 0xde00, 1);
if (GetWindowTextLengthW(editor) != (int)length + 3) {
fprintf(stderr, "large typing: length=%d expected=%lu limit=%ld\n",
GetWindowTextLengthW(editor), (unsigned long)length + 3,
(long)SendMessageW(editor, EM_GETLIMITTEXT, 0, 0));
ExitProcess(2);
}
}
break;
case 40:
SetWindowTextW(editor, L"full");
SendMessageW(editor, EM_SETSEL, 4, 4);
SendMessageW(editor, EM_SETLIMITTEXT, 4, 0);
SendMessageW(editor, WM_CHAR, L'%', 1);
if (GetWindowTextLengthW(editor) != 4) ExitProcess(2);
break;
default:
DestroyWindow(window); break;
}
+7 -2
View File
@@ -251,8 +251,10 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
"N unselected", "N deleted"):
assert expected in spoken, f"missing Notepad feedback: {expected!r}; {text_events!r}"
assert spoken.count("l") == 2, "Notepad lost repeated characters"
typed = [e[2] for e in text_events if e[1] == "text" and "kind=8" in e[5]]
assert typed == ["z", "z", "space", "new line"], f"Notepad typing echo missing or incorrect: {typed!r}"
assert any(e[3] == "edit" and e[4] == "otepad second line" for e in text_events), "Notepad menu return missing"
print("PASS: actual Wine Notepad keyboard characters, words, lines, selection, deletion, and menu return")
print("PASS: actual Wine Notepad typing echo, keyboard characters, words, lines, selection, deletion, and menu return")
sys.exit(0)
if args.fixture == "scintilla":
textEvents = [e for e in events if e[0].endswith(":text")]
@@ -302,13 +304,16 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
for line in ("readonly first", "readonly second"):
assert any(e[1] == "text" and e[2] == line for e in text_events), f"read-only arrow feedback missing: {line}"
assert "r deleted" not in spoken, "read-only document reported an unsuccessful deletion"
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"
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"
assert any(e[2] == "Password" and e[3] == "edit" and e[6] == "password"
and e[4] == "" for e in text_events), "managed password label/protection missing"
assert not any(e[2] == "Unrelated" for e in text_events), "unrelated label associated with field"
print("PASS: editor focus, characters, words, lines, selection, deletion, Unicode, passwords, and read-only controls")
print("PASS: editor focus, typing echo, characters, words, lines, selection, deletion, Unicode, passwords, and read-only controls")
sys.exit(0)
menus = [event for event in events if ":menu:" in event[0]]
for event in menus:
+15 -1
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
// Drive Wine's actual Notepad; the standard-control fixture covers edge cases.
#include <windows.h>
#include <stdio.h>
static DWORD notepad_pid;
static HWND notepad;
@@ -61,6 +62,18 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
if (notepad) {
HWND edit = FindWindowExW(notepad, NULL, L"Edit", NULL);
SetForegroundWindow(notepad);
Sleep(400);
key('Z', FALSE, FALSE);
key('Z', FALSE, FALSE);
key(VK_SPACE, FALSE, FALSE);
key(VK_RETURN, FALSE, FALSE);
WCHAR typed[16] = {0};
SendMessageW(edit, WM_GETTEXT, 16, (LPARAM)typed);
if (lstrcmpW(typed, L"zz \r\n")) {
fprintf(stderr, "Notepad typing did not reach its edit: units=%d first=%04x foreground=%p expected=%p\n",
lstrlenW(typed), (unsigned)typed[0], (void *)GetForegroundWindow(), (void *)notepad);
ExitProcess(2);
}
SendMessageW(edit, WM_SETTEXT, 0, (LPARAM)L"hello beta\r\nNotepad second line");
SendMessageW(edit, EM_SETSEL, 0, 0);
Sleep(400);
@@ -79,12 +92,13 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
WCHAR text[80] = {0};
SendMessageW(edit, WM_GETTEXT, 80, (LPARAM)text);
if (!lstrcmpW(text, L"hello beta\r\notepad second line")) result = 0;
else fprintf(stderr, "Notepad final document mismatch: units=%d\n", lstrlenW(text));
PostMessageW(notepad, WM_CLOSE, 0, 0);
for (int i = 0; i < 100 && !saveDialog; ++i) {
EnumWindows(find_save_dialog, 0);
Sleep(50);
}
if (!saveDialog) result = 2;
if (!saveDialog) { fprintf(stderr, "Notepad save dialog missing\n"); result = 2; }
else {
SetForegroundWindow(saveDialog);
Sleep(400);