336 lines
13 KiB
C
336 lines
13 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;
|
|
|
|
static DWORD WINAPI update_history_from_worker(void *window) {
|
|
SendMessageW((HWND)window, WM_SETTEXT, 0, (LPARAM)L"unrelated worker update");
|
|
return 0;
|
|
}
|
|
|
|
static void unrelated_history_update(void) {
|
|
HANDLE thread = CreateThread(NULL, 0, update_history_from_worker, history, 0, NULL);
|
|
if (!thread) ExitProcess(2);
|
|
// Dispatch sent messages without removing a queued message, as GetMessage
|
|
// does while waiting for input. Do not rely on worker/timer scheduling.
|
|
for (;;) {
|
|
DWORD result = MsgWaitForMultipleObjects(1, &thread, FALSE, 2000, QS_SENDMESSAGE);
|
|
if (result == WAIT_OBJECT_0) break;
|
|
if (result != WAIT_OBJECT_0 + 1) ExitProcess(2);
|
|
MSG message;
|
|
PeekMessageW(&message, NULL, 0, 0, PM_NOREMOVE);
|
|
}
|
|
CloseHandle(thread);
|
|
WCHAR text[64];
|
|
GetWindowTextW(history, text, 64);
|
|
if (lstrcmpW(text, L"unrelated worker update")) ExitProcess(2);
|
|
}
|
|
|
|
// 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 {
|
|
if (step == 49) unrelated_history_update();
|
|
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:
|
|
// The silent worker update must still refresh the navigation snapshot.
|
|
PostMessageW(history, WM_KEYDOWN, VK_RIGHT, 1);
|
|
break;
|
|
case 51:
|
|
SendMessageW(history, EM_SETPASSWORDCHAR, L'*', 0);
|
|
PostMessageW(history, WM_KEYDOWN, VK_UP, 1);
|
|
break;
|
|
case 52:
|
|
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;
|
|
}
|