Read command-history recalls in Wine edit fields
MUSHclient consumes queued Up/Down keys in PreTranslateMessage before the Edit control receives WM_KEYDOWN. It recalls commands with SetWindowText followed by SetSel at the end of the replacement. The editor subclass only refreshed its snapshot for WM_SETTEXT, so the recalled command was silent or followed by a stray character or blank from the caret update. Recalling equal-length commands also leaves the final caret position unchanged. Observe Up/Down for the focused standard Edit control in WH_GETMESSAGE, excluding Ctrl-modified navigation. Associate the next successful WM_SETTEXT with that retrieved key and announce the bounded recalled text as an existing ordered text-read action. Report empty input as blank. Consume the pending recall after the replacement and suppress the ensuing EM_SETSEL feedback so each recalled command is announced once. Reset recall tracking on subsequent retrieved messages, actual focus and ordinary dispatched navigation. Preserve snapshot refreshes and normal Left/Right reading after recalls. Keep password and foreground/focus exclusion in place, leave unrelated programmatic text updates silent, and check the control class before accessing the shared attachment property so Scintilla's different state structure is untouched. Both native hook architectures receive the fix through the existing build. Extend the Edit fixture with application-side pretranslation of queued history arrows. Assert the exact ordered feedback for Up/Down, equal-length entries, rapid recalls, empty input and horizontal navigation afterward. Require silence for unchanged history boundaries, unrelated programmatic updates, protected fields and independently unprotected background fields. Document history behavior and live MUSHclient acceptance steps. Validation used a private Xorg dummy display, disposable Wine prefixes and recording D-Bus services, with GUI checks run sequentially: - cmake --build build --parallel - ctest --test-dir build --output-on-failure: all four checks passed - python3 tests/menu_integration.py --build-dir build --fixture edit - The Edit check with --32-bit, --32-bit --late-helper, and --32-bit --restart-helper - python3 tests/menu_integration.py --build-dir build --fixture winforms --32-bit - python3 tests/menu_integration.py --build-dir build --fixture notepad - python3 tests/menu_integration.py --build-dir build --fixture menu --32-bit - git diff --check The new 32-bit history regression failed against the original hook, recording only blank actions, and passed with the fix. Final 32-bit, 64-bit and Windows Forms editor checks passed, including existing typing, selection, deletion, Unicode, password and read-only assertions. Built and installed the helper into the existing user-local installation, backed up the previous five files and verified the installed artifacts. The user subsequently confirmed that the installed fix works in MUSHclient. Physical braille acceptance was not separately established.
This commit is contained in:
1 parent
7d3ac09ea1
commit
b808c6686a
5 files changed
+112
-12
No files matched your search
@@ -197,6 +197,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
|
characters and words on horizontal navigation, lines on vertical/page/document-boundary
|
||||||
navigation, changed selections, and Backspace/Delete/Cut feedback. Password
|
navigation, changed selections, and Backspace/Delete/Cut feedback. Password
|
||||||
contents are excluded, and read-only edits do not report unsuccessful deletions.
|
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
|
Undo refreshes the text snapshot used by subsequent navigation. It can attach to
|
||||||
an already-focused editor and resume after the helper restarts.
|
an already-focused editor and resume after the helper restarts.
|
||||||
|
|
||||||
|
|||||||
+29
-1
@@ -14,6 +14,7 @@ typedef struct EditState {
|
|||||||
WCHAR *text;
|
WCHAR *text;
|
||||||
DWORD length;
|
DWORD length;
|
||||||
BOOL busy, known, destroyed;
|
BOOL busy, known, destroyed;
|
||||||
|
BOOL history_key, history_replaced;
|
||||||
WCHAR pending_high;
|
WCHAR pending_high;
|
||||||
DWORD pending_caret;
|
DWORD pending_caret;
|
||||||
} EditState;
|
} EditState;
|
||||||
@@ -101,6 +102,10 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
|||||||
BOOL requested_focus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE);
|
BOOL requested_focus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE);
|
||||||
BOOL focus = message == WM_SETFOCUS || requested_focus;
|
BOOL focus = message == WM_SETFOCUS || requested_focus;
|
||||||
BOOL selected = message == EM_SETSEL;
|
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)
|
if (!navigation && !changing && !focus && !selected && message != EM_SETPASSWORDCHAR)
|
||||||
return DefSubclassProc(window, message, wp, lp);
|
return DefSubclassProc(window, message, wp, lp);
|
||||||
|
|
||||||
@@ -151,7 +156,17 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
|||||||
if (focus) {
|
if (focus) {
|
||||||
current_line(window, start, text);
|
current_line(window, start, text);
|
||||||
kind = WINE_ACCESS_TEXT_FOCUS; report = TRUE;
|
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) {
|
if (start != end && state->text && end <= state->length) {
|
||||||
// Speak the changed portion when extending or shrinking a selection.
|
// Speak the changed portion when extending or shrinking a selection.
|
||||||
DWORD first = start, last = end;
|
DWORD first = start, last = end;
|
||||||
@@ -260,6 +275,19 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp,
|
|||||||
HWND focus = GetFocus();
|
HWND focus = GetFocus();
|
||||||
if (wine_access_watch_edit(focus))
|
if (wine_access_watch_edit(focus))
|
||||||
SendMessageW(focus, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
|
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
|
#ifdef _WIN64
|
||||||
wine_access_scintilla_input(focus, (const MSG *)lp);
|
wine_access_scintilla_input(focus, (const MSG *)lp);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+17
-10
@@ -262,9 +262,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
|
control. Typing checks include repeated letters, spaces, Enter/Tab, accented
|
||||||
characters, surrogate pairs, selection replacement, documents beyond the full
|
characters, surrogate pairs, selection replacement, documents beyond the full
|
||||||
snapshot limit, and silence for password/background/read-only fields and
|
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
|
input rejected at the text limit. Command-history checks consume queued Up/Down
|
||||||
assertions. Lifecycle variants attach after the control has focus or
|
before dispatch, then replace the text and move the caret as MUSHclient does.
|
||||||
restart the helper without closing it. Every text action must have an established
|
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.
|
focus source, matching the Go reader's filtering contract.
|
||||||
|
|
||||||
The Notepad fixture launches Wine's actual `notepad.exe` and uses `SendInput`
|
The Notepad fixture launches Wine's actual `notepad.exe` and uses `SendInput`
|
||||||
@@ -273,13 +277,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
|
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.
|
audible typing echo, physical keys, or a physical braille display.
|
||||||
|
|
||||||
For live MUSHclient acceptance, install the helper and reopen MUSHclient so the
|
For live MUSHclient acceptance, install the helper, restart Cthulhu and reopen
|
||||||
updated 32-bit DLL can load. In the command field, check Left/Right and
|
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
|
Ctrl+Left/Right feedback without submitting a command. Use Up/Down to recall
|
||||||
the read-only output notepad supplied by the Aardwolf review-buffer plugin;
|
existing command history. Check each recalled command is spoken once,
|
||||||
check Up/Down line reading and Left/Right characters there, then return to
|
including equal-length entries and `blank` when returning to empty input.
|
||||||
the command field. Check plugin menus and dialogs still work, and repeat after
|
Do not submit recalled commands just to test speech.
|
||||||
restarting the reader. These hooks provide caret feedback; independent review
|
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
|
commands use the optional flat-review protocol described above. Native braille
|
||||||
routing into Wine text remains unsupported.
|
routing into Wine text remains unsupported.
|
||||||
|
|
||||||
|
|||||||
+55
-1
@@ -18,8 +18,24 @@ static void register_control(HINSTANCE instance, const WCHAR *nativeName, const
|
|||||||
static const WCHAR *editClass = L"Edit";
|
static const WCHAR *editClass = L"Edit";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static HWND editor, password;
|
static HWND editor, password, history;
|
||||||
static int step;
|
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) {
|
static void key(UINT key, BOOL control, BOOL shift) {
|
||||||
BYTE saved[256], state[256];
|
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);
|
SendMessageW(editor, WM_CHAR, L'%', 1);
|
||||||
if (GetWindowTextLengthW(editor) != 4) ExitProcess(2);
|
if (GetWindowTextLengthW(editor) != 4) ExitProcess(2);
|
||||||
break;
|
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:
|
default:
|
||||||
DestroyWindow(window); break;
|
DestroyWindow(window); break;
|
||||||
}
|
}
|
||||||
@@ -245,6 +298,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
|||||||
SetTimer(window, 1, 1500, NULL);
|
SetTimer(window, 1, 1500, NULL);
|
||||||
MSG message;
|
MSG message;
|
||||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||||
|
if (history_input(&message)) continue;
|
||||||
TranslateMessage(&message); DispatchMessageW(&message);
|
TranslateMessage(&message); DispatchMessageW(&message);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -307,6 +307,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]]
|
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 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"
|
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":
|
if args.fixture == "winforms":
|
||||||
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
|
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
|
||||||
for e in text_events), "managed email label/focus missing"
|
for e in text_events), "managed email label/focus missing"
|
||||||
|
|||||||
Reference in new issue
Block a user