Echo typed characters in Wine edit fields
This commit is contained in:
1 parent
d9aa40e16c
commit
a002c4cc1e
7 files changed
+154
-22
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user