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.
306 lines
12 KiB
C
306 lines
12 KiB
C
// 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";
|
|
static const WCHAR *staticClass = L"WindowsForms10.STATIC.app.0.fixture_r7_ad1";
|
|
static void register_control(HINSTANCE instance, const WCHAR *nativeName, const WCHAR *managedName) {
|
|
WNDCLASSEXW cls = {0};
|
|
cls.cbSize = sizeof(cls);
|
|
if (!GetClassInfoExW(NULL, nativeName, &cls)) ExitProcess(3);
|
|
cls.hInstance = instance;
|
|
cls.lpszClassName = managedName;
|
|
if (!RegisterClassExW(&cls)) ExitProcess(4);
|
|
}
|
|
#else
|
|
static const WCHAR *editClass = L"Edit";
|
|
#endif
|
|
|
|
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];
|
|
GetKeyboardState(saved);
|
|
CopyMemory(state, saved, sizeof(state));
|
|
state[VK_CONTROL] = control ? 0x80 : 0;
|
|
state[VK_SHIFT] = shift ? 0x80 : 0;
|
|
SetKeyboardState(state);
|
|
SendMessageW(editor, WM_KEYDOWN, key, 1);
|
|
SetKeyboardState(saved);
|
|
}
|
|
|
|
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
|
if (message == WM_TIMER) {
|
|
if (step == 0) SetTimer(window, 1, 350, NULL);
|
|
switch (++step) {
|
|
case 1: key(VK_RIGHT, FALSE, FALSE); break;
|
|
case 2: key(VK_RIGHT, TRUE, FALSE); break;
|
|
case 3: key(VK_DOWN, FALSE, FALSE); break;
|
|
case 4: key(VK_HOME, FALSE, FALSE); break;
|
|
case 5: key(VK_RIGHT, FALSE, TRUE); break;
|
|
case 6: key(VK_RIGHT, FALSE, TRUE); break;
|
|
case 7: key(VK_LEFT, FALSE, TRUE); break;
|
|
case 8: key(VK_LEFT, FALSE, FALSE); break;
|
|
case 9: key(VK_DELETE, FALSE, FALSE); break;
|
|
case 10:
|
|
key(VK_RIGHT, FALSE, FALSE);
|
|
SendMessageW(editor, WM_CHAR, VK_BACK, 1);
|
|
break;
|
|
case 11: SendMessageW(editor, EM_SETSEL, 0, 5); break;
|
|
case 12: SendMessageW(editor, WM_CLEAR, 0, 0); break;
|
|
case 13: SetFocus(password); break;
|
|
case 14: SendMessageW(password, WM_KEYDOWN, VK_LEFT, 1); break;
|
|
case 15: SendMessageW(password, WM_CHAR, VK_BACK, 1); break;
|
|
case 16:
|
|
SetFocus(editor);
|
|
SetWindowTextW(editor, L"\U0001f600 cafe");
|
|
SendMessageW(editor, EM_SETSEL, 2, 2);
|
|
break;
|
|
case 17: key(VK_HOME, FALSE, FALSE); break;
|
|
case 18:
|
|
SendMessageW(editor, EM_SETREADONLY, TRUE, 0);
|
|
SetFocus(password); SetFocus(editor);
|
|
break;
|
|
case 19: key(VK_DELETE, FALSE, FALSE); break;
|
|
case 20:
|
|
if (GetWindowTextLengthW(editor) != 7) ExitProcess(2);
|
|
SendMessageW(editor, EM_SETREADONLY, FALSE, 0);
|
|
SetWindowTextW(editor, L"hello\r\nmenu return line");
|
|
SendMessageW(editor, EM_SETSEL, 0, 0);
|
|
break;
|
|
case 21:
|
|
key(VK_RIGHT, FALSE, FALSE);
|
|
key(VK_RIGHT, FALSE, FALSE);
|
|
key(VK_RIGHT, FALSE, FALSE);
|
|
break;
|
|
case 22:
|
|
SendMessageW(editor, EM_SETSEL, 7, 7);
|
|
SendMessageW(window, WM_MENUSELECT, MAKEWPARAM(1, 0), (LPARAM)GetMenu(window));
|
|
break;
|
|
case 23: SendMessageW(window, WM_EXITMENULOOP, 0, 0); break;
|
|
case 24:
|
|
// Replacement with shorter text is not a Delete/Backspace command.
|
|
SendMessageW(editor, EM_SETSEL, 0, 5);
|
|
SendMessageW(editor, EM_REPLACESEL, TRUE, (LPARAM)L"hi");
|
|
break;
|
|
case 25:
|
|
SetWindowTextW(editor, L"first\r\nsecond");
|
|
SendMessageW(editor, EM_SETSEL, 7, 7);
|
|
SendMessageW(editor, WM_CHAR, VK_BACK, 1);
|
|
break;
|
|
case 26:
|
|
SetWindowTextW(editor, L"first\r\nsecond");
|
|
SendMessageW(editor, EM_SETSEL, 5, 5);
|
|
key(VK_DELETE, FALSE, FALSE);
|
|
break;
|
|
case 27:
|
|
// A surrogate at the 1023-unit line boundary must not discard the line.
|
|
{
|
|
WCHAR line[1027];
|
|
for (int i = 0; i < 1022; ++i) line[i] = L'x';
|
|
line[1022] = 0xd83d; line[1023] = 0xde00;
|
|
line[1024] = L'y'; line[1025] = 0;
|
|
SetWindowTextW(editor, line);
|
|
SendMessageW(editor, EM_SETSEL, 0, 0);
|
|
SetFocus(password); SetFocus(editor);
|
|
}
|
|
break;
|
|
case 28:
|
|
SetWindowTextW(editor, L"undo restored");
|
|
SendMessageW(editor, EM_SETSEL, 0, 4);
|
|
SendMessageW(editor, EM_REPLACESEL, TRUE, (LPARAM)L"x");
|
|
SendMessageW(editor, EM_UNDO, 0, 0);
|
|
SendMessageW(editor, EM_SETSEL, 1, 1);
|
|
break;
|
|
case 29:
|
|
key(VK_RIGHT, FALSE, FALSE); // d, not the space in the old snapshot
|
|
break;
|
|
case 30:
|
|
key(VK_END, TRUE, FALSE); // whole restored line
|
|
break;
|
|
case 31:
|
|
SetWindowTextW(editor, L"readonly first\r\nreadonly second");
|
|
SendMessageW(editor, EM_SETSEL, 0, 0);
|
|
SendMessageW(editor, EM_SETREADONLY, TRUE, 0);
|
|
SetFocus(password); SetFocus(editor);
|
|
break;
|
|
case 32: key(VK_DOWN, FALSE, FALSE); break;
|
|
case 33: key(VK_UP, FALSE, FALSE); break;
|
|
case 34:
|
|
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;
|
|
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;
|
|
}
|
|
return 0;
|
|
}
|
|
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
|
return DefWindowProcW(window, message, wp, lp);
|
|
}
|
|
|
|
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show) {
|
|
(void)previous; (void)args; (void)show;
|
|
#ifdef WINE_ACCESS_WINFORMS_FIXTURE
|
|
register_control(instance, L"Edit", editClass);
|
|
register_control(instance, L"Static", staticClass);
|
|
#endif
|
|
WNDCLASSW cls = {0};
|
|
cls.hInstance = instance; cls.lpszClassName = L"WineAccessEditFixture";
|
|
cls.lpfnWndProc = window_proc; RegisterClassW(&cls);
|
|
HWND window = CreateWindowW(cls.lpszClassName, L"Wine editor fixture", WS_OVERLAPPEDWINDOW,
|
|
120, 120, 500, 300, NULL, NULL, instance, NULL);
|
|
editor = CreateWindowW(editClass, L"alpha beta\r\nsecond line",
|
|
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL,
|
|
100, 10, 350, 150, window, NULL, instance, NULL);
|
|
password = CreateWindowW(editClass, L"private-password-fixture",
|
|
WS_CHILD | WS_VISIBLE | ES_PASSWORD, 100, 180, 300, 30,
|
|
window, NULL, instance, NULL);
|
|
#ifdef WINE_ACCESS_WINFORMS_FIXTURE
|
|
// Labels are created after the fields, like the observed BARD login.
|
|
CreateWindowW(staticClass, L"Password:", WS_CHILD | WS_VISIBLE, 10, 183, 85, 20,
|
|
window, NULL, instance, NULL);
|
|
CreateWindowW(staticClass, L"Email:", WS_CHILD | WS_VISIBLE, 10, 13, 60, 20,
|
|
window, NULL, instance, NULL);
|
|
CreateWindowW(staticClass, L"Unrelated:", WS_CHILD | WS_VISIBLE, 10, 220, 80, 20,
|
|
window, NULL, instance, NULL);
|
|
#endif
|
|
HMENU menu = CreateMenu();
|
|
AppendMenuW(menu, MF_STRING, 1, L"Test menu");
|
|
SetMenu(window, menu);
|
|
ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(editor);
|
|
SendMessageW(editor, EM_SETSEL, 0, 0);
|
|
WCHAR ready[1024];
|
|
if (GetEnvironmentVariableW(L"WINE_ACCESS_FIXTURE_READY", ready, 1024)) {
|
|
HANDLE file = CreateFileW(ready, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL);
|
|
if (file == INVALID_HANDLE_VALUE) return 2;
|
|
CloseHandle(file);
|
|
}
|
|
// Leave time to attach/restart a helper while this already-focused window
|
|
// keeps pumping messages. Subsequent actions use the ordinary short timer.
|
|
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;
|
|
}
|