Work started on editor support.
This commit is contained in:
1 parent
0992ffee0d
commit
6f6d4040c4
9 files changed
+401
-5
No files matched your search
+2
-2
@@ -15,8 +15,8 @@ set(menu_hook "${CMAKE_CURRENT_BINARY_DIR}/wine-access-menu.dll")
|
||||
add_custom_command(OUTPUT "${menu_hook}"
|
||||
COMMAND "${MINGW_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -shared
|
||||
-static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
||||
-luser32
|
||||
DEPENDS menu_hook.c menu_bridge.h
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c" -luser32 -lcomctl32
|
||||
DEPENDS menu_hook.c menu_bridge.h edit_hook.c edit_hook.h
|
||||
VERBATIM)
|
||||
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
pkgname=wine-access-git
|
||||
pkgver=r3.gf365af3
|
||||
pkgver=r6.g0992ffe
|
||||
pkgrel=1
|
||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
||||
arch=('x86_64')
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "edit_hook.h"
|
||||
#include "menu_bridge.h"
|
||||
#include <commctrl.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define EDIT_SUBCLASS_ID 0x57415431
|
||||
#define EDIT_STATE_PROPERTY L"CthulhuWineEditState"
|
||||
// Bound memory and work inside applications. Navigation reuses the snapshot;
|
||||
// only text changes refresh it. Large documents retain line/character reading.
|
||||
#define MAX_SNAPSHOT_UNITS (1024 * 1024)
|
||||
|
||||
typedef struct EditState {
|
||||
WCHAR *text;
|
||||
DWORD length, start, end;
|
||||
BOOL busy, known, destroyed;
|
||||
} EditState;
|
||||
|
||||
static void selection(HWND window, DWORD *start, DWORD *end) {
|
||||
SendMessageW(window, EM_GETSEL, (WPARAM)start, (LPARAM)end);
|
||||
}
|
||||
|
||||
static void snapshot(HWND window, EditState *state) {
|
||||
free(state->text);
|
||||
state->text = NULL;
|
||||
state->length = GetWindowTextLengthW(window);
|
||||
if (state->length <= MAX_SNAPSHOT_UNITS) {
|
||||
state->text = calloc((size_t)state->length + 1, sizeof(WCHAR));
|
||||
if (state->text)
|
||||
state->length = GetWindowTextW(window, state->text, state->length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void copy_range(WCHAR *output, const WCHAR *text, DWORD start, DWORD end) {
|
||||
UINT used = 0;
|
||||
while (start < end && used < 1023) output[used++] = text[start++];
|
||||
// Never leave half a UTF-16 surrogate pair at the truncation boundary.
|
||||
if (used && output[used - 1] >= 0xd800 && output[used - 1] <= 0xdbff) --used;
|
||||
output[used] = 0;
|
||||
}
|
||||
|
||||
static void current_line(HWND window, DWORD caret, WCHAR *output) {
|
||||
LRESULT line = SendMessageW(window, EM_LINEFROMCHAR, caret, 0);
|
||||
// EM_GETLINE expects the buffer capacity in its first WORD.
|
||||
output[0] = 1023;
|
||||
LRESULT copied = SendMessageW(window, EM_GETLINE, line, (LPARAM)output);
|
||||
if (copied < 0 || copied > 1023) copied = 0;
|
||||
output[copied] = 0;
|
||||
}
|
||||
|
||||
static BOOL word_space(WCHAR ch) {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
|
||||
}
|
||||
|
||||
static void notify_text(HWND window, UINT kind, const WCHAR *text, DWORD start, DWORD end) {
|
||||
HWND owner = GetAncestor(window, GA_ROOT);
|
||||
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
||||
if (!bridge || owner != GetAncestor(GetForegroundWindow(), GA_ROOT) || GetFocus() != window)
|
||||
return;
|
||||
WineAccessTextEvent event = {0};
|
||||
event.owner = (ULONGLONG)(UINT_PTR)owner;
|
||||
event.control = (ULONGLONG)(UINT_PTR)window;
|
||||
event.kind = kind; event.start = start; event.end = end;
|
||||
lstrcpynW(event.text, text, 1024);
|
||||
GetWindowTextW(owner, event.title, 256);
|
||||
COPYDATASTRUCT data = {WINE_ACCESS_TEXT_TAG, sizeof(event), &event};
|
||||
DWORD_PTR result;
|
||||
SendMessageTimeoutW(bridge, WM_COPYDATA, (WPARAM)window, (LPARAM)&data,
|
||||
SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM lp,
|
||||
UINT_PTR id, DWORD_PTR reference) {
|
||||
EditState *state = (EditState *)reference;
|
||||
if (message == WM_NCDESTROY) {
|
||||
RemoveWindowSubclass(window, edit_proc, id);
|
||||
RemovePropW(window, EDIT_STATE_PROPERTY);
|
||||
state->destroyed = TRUE;
|
||||
if (!state->busy) { free(state->text); free(state); }
|
||||
return DefSubclassProc(window, message, wp, lp);
|
||||
}
|
||||
if (state->busy) return DefSubclassProc(window, message, wp, lp);
|
||||
BOOL navigation = message == WM_KEYDOWN &&
|
||||
(wp == VK_LEFT || wp == VK_RIGHT || wp == VK_UP || wp == VK_DOWN ||
|
||||
wp == VK_HOME || wp == VK_END || wp == VK_PRIOR || wp == VK_NEXT);
|
||||
BOOL changing = message == WM_CHAR || (message == WM_KEYDOWN && wp == VK_DELETE) ||
|
||||
message == WM_PASTE || message == WM_CUT || message == WM_CLEAR ||
|
||||
message == WM_UNDO || message == EM_REPLACESEL || message == WM_SETTEXT;
|
||||
BOOL focus = message == WM_SETFOCUS;
|
||||
BOOL selected = message == EM_SETSEL;
|
||||
if (!navigation && !changing && !focus && !selected && message != EM_SETPASSWORDCHAR)
|
||||
return DefSubclassProc(window, message, wp, lp);
|
||||
|
||||
state->busy = TRUE;
|
||||
DWORD old_start = 0, old_end = 0;
|
||||
selection(window, &old_start, &old_end);
|
||||
BOOL password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0;
|
||||
if (!password && (!state->known || focus)) snapshot(window, state);
|
||||
WCHAR removed[1024] = {0};
|
||||
if (!password && state->text && changing) {
|
||||
DWORD first = old_start, last = old_end;
|
||||
if (first == last) {
|
||||
if (message == WM_CHAR && wp == VK_BACK && first) {
|
||||
--first;
|
||||
if (first && state->text[first] >= 0xdc00 && state->text[first] <= 0xdfff) --first;
|
||||
} else if (message == WM_KEYDOWN && wp == VK_DELETE && last < state->length) {
|
||||
++last;
|
||||
if (last < state->length && state->text[last - 1] >= 0xd800 && state->text[last - 1] <= 0xdbff) ++last;
|
||||
}
|
||||
}
|
||||
if (last <= state->length) copy_range(removed, state->text, first, last);
|
||||
}
|
||||
LRESULT result = DefSubclassProc(window, message, wp, lp);
|
||||
if (state->destroyed) {
|
||||
free(state->text); free(state);
|
||||
return result;
|
||||
}
|
||||
DWORD start = 0, end = 0;
|
||||
selection(window, &start, &end);
|
||||
password = (GetWindowLongPtrW(window, GWL_STYLE) & ES_PASSWORD) != 0;
|
||||
if (password) {
|
||||
free(state->text); state->text = NULL; state->known = FALSE;
|
||||
if (focus) notify_text(window, WINE_ACCESS_TEXT_PASSWORD, L"", start, end);
|
||||
state->busy = FALSE;
|
||||
return result;
|
||||
}
|
||||
DWORD old_length = state->length;
|
||||
if (changing || message == EM_SETPASSWORDCHAR) snapshot(window, state);
|
||||
state->known = TRUE;
|
||||
WCHAR text[1024] = {0};
|
||||
UINT kind = WINE_ACCESS_TEXT_READ;
|
||||
BOOL report = FALSE;
|
||||
if (focus) {
|
||||
current_line(window, start, text);
|
||||
kind = WINE_ACCESS_TEXT_FOCUS; report = TRUE;
|
||||
} else if ((navigation || selected) && (start != old_start || end != old_end)) {
|
||||
if (start != end && state->text && end <= state->length) {
|
||||
// Speak the changed portion when extending or shrinking a selection.
|
||||
DWORD first = start, last = end;
|
||||
kind = WINE_ACCESS_TEXT_SELECTED;
|
||||
if (old_start != old_end && start == old_start) {
|
||||
first = end < old_end ? end : old_end;
|
||||
last = end > old_end ? end : old_end;
|
||||
if (end < old_end) kind = WINE_ACCESS_TEXT_UNSELECTED;
|
||||
} else if (old_start != old_end && end == old_end) {
|
||||
first = start < old_start ? start : old_start;
|
||||
last = start > old_start ? start : old_start;
|
||||
if (start > old_start) kind = WINE_ACCESS_TEXT_UNSELECTED;
|
||||
}
|
||||
copy_range(text, state->text, first, last);
|
||||
} else if (old_start != old_end && state->text && old_end <= state->length) {
|
||||
copy_range(text, state->text, old_start, old_end);
|
||||
kind = WINE_ACCESS_TEXT_UNSELECTED;
|
||||
} else if (navigation && (wp == VK_UP || wp == VK_DOWN || wp == VK_PRIOR || wp == VK_NEXT)) {
|
||||
current_line(window, start, text);
|
||||
} else if (state->text && start < state->length) {
|
||||
DWORD last = start + 1;
|
||||
if (GetKeyState(VK_CONTROL) & 0x8000) {
|
||||
while (last < state->length && !word_space(state->text[last])) ++last;
|
||||
} else if (state->text[start] >= 0xd800 && state->text[start] <= 0xdbff && last < state->length) {
|
||||
++last;
|
||||
}
|
||||
copy_range(text, state->text, start, last);
|
||||
} else if (!state->text) {
|
||||
current_line(window, start, text);
|
||||
}
|
||||
report = TRUE;
|
||||
} else if (changing && removed[0] && old_length > state->length) {
|
||||
lstrcpynW(text, removed, 1024);
|
||||
kind = WINE_ACCESS_TEXT_DELETED; report = TRUE;
|
||||
}
|
||||
if (report) notify_text(window, kind, text, start, end);
|
||||
state->start = start; state->end = end;
|
||||
state->busy = FALSE;
|
||||
return result;
|
||||
}
|
||||
|
||||
void wine_access_watch_edit(HWND window) {
|
||||
WCHAR class_name[64];
|
||||
if (!window || !GetClassNameW(window, class_name, 64) || lstrcmpW(class_name, L"Edit")) return;
|
||||
// Wine's comctl32 v5 exports GetWindowSubclass only by ordinal. Our own
|
||||
// property also keeps attachment independent of the application's manifest.
|
||||
if (GetPropW(window, EDIT_STATE_PROPERTY)) return;
|
||||
EditState *state = calloc(1, sizeof(*state));
|
||||
if (!state) return;
|
||||
if (!SetWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID, (DWORD_PTR)state)) {
|
||||
free(state); return;
|
||||
}
|
||||
if (!SetPropW(window, EDIT_STATE_PROPERTY, state)) {
|
||||
RemoveWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID);
|
||||
free(state); return;
|
||||
}
|
||||
// Subclass callbacks can outlive the reader. Keep their module mapped until
|
||||
// the application exits; each control frees its state on WM_NCDESTROY.
|
||||
HMODULE module;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
|
||||
(LPCWSTR)(UINT_PTR)edit_proc, &module);
|
||||
}
|
||||
|
||||
__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) {
|
||||
if (code >= 0 && wp == PM_REMOVE) wine_access_watch_edit(GetFocus());
|
||||
return CallNextHookEx(NULL, code, wp, lp);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
void wine_access_watch_edit(HWND window);
|
||||
@@ -24,6 +24,8 @@ constexpr auto INTERFACE_NAME = "org.stormux.Cthulhu1.Service";
|
||||
constexpr int DBUS_TIMEOUT_MS = 500;
|
||||
GDBusConnection *connection = nullptr;
|
||||
std::deque<WineAccessMenuEvent> menuEvents;
|
||||
std::deque<WineAccessTextEvent> textEvents;
|
||||
bool editReaderActive = false;
|
||||
std::string lastMenuEvent;
|
||||
ULONGLONG lastMenuOwner = 0;
|
||||
|
||||
@@ -360,9 +362,59 @@ void present_menu_event(const WineAccessMenuEvent &event) {
|
||||
}
|
||||
}
|
||||
|
||||
void present_text_event(const WineAccessTextEvent &event) {
|
||||
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
|
||||
HWND window = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.control));
|
||||
GUITHREADINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
if (owner == nullptr || owner != GetAncestor(GetForegroundWindow(), GA_ROOT) ||
|
||||
!GetGUIThreadInfo(GetWindowThreadProcessId(owner, nullptr), &info) || info.hwndFocus != window)
|
||||
return;
|
||||
std::string text = to_utf8(event.text);
|
||||
ControlDescription control;
|
||||
if (event.kind == WINE_ACCESS_TEXT_PASSWORD) {
|
||||
control.role = "edit";
|
||||
control.description = "password";
|
||||
} else if (event.kind == WINE_ACCESS_TEXT_FOCUS) {
|
||||
wchar_t label[256] = {};
|
||||
find_control_label(window, label, std::size(label));
|
||||
control.name = to_utf8(label);
|
||||
control.role = GetWindowLongPtrW(window, GWL_STYLE) & ES_READONLY ? "read only edit" : "edit";
|
||||
control.value = text.empty() ? "blank" : text;
|
||||
} else {
|
||||
if (text == " ") text = "space";
|
||||
else if (text == "\t") text = "tab";
|
||||
else if (text == "\r" || text == "\n" || text == "\r\n") text = "new line";
|
||||
const char *format = nullptr;
|
||||
if (event.kind == WINE_ACCESS_TEXT_SELECTED) format = "%s selected";
|
||||
else if (event.kind == WINE_ACCESS_TEXT_UNSELECTED) format = "%s unselected";
|
||||
else if (event.kind == WINE_ACCESS_TEXT_DELETED) format = "%s deleted";
|
||||
if (format != nullptr) {
|
||||
gchar *message = g_strdup_printf(format, text.c_str());
|
||||
control.name = message;
|
||||
g_free(message);
|
||||
} else {
|
||||
control.name = text.empty() ? "blank" : text;
|
||||
}
|
||||
}
|
||||
present_accessible_event(source_id(window, "text"), "focus", control, to_utf8(event.title));
|
||||
}
|
||||
|
||||
LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_COPYDATA && lp != 0) {
|
||||
const auto *data = reinterpret_cast<const COPYDATASTRUCT *>(lp);
|
||||
if (data->dwData == WINE_ACCESS_TEXT_TAG && data->cbData == sizeof(WineAccessTextEvent) &&
|
||||
data->lpData != nullptr && textEvents.size() < 32) {
|
||||
auto event = *static_cast<const WineAccessTextEvent *>(data->lpData);
|
||||
event.text[std::size(event.text) - 1] = L'\0';
|
||||
event.title[std::size(event.title) - 1] = L'\0';
|
||||
textEvents.push_back(event);
|
||||
if (!PostMessageW(window, WINE_ACCESS_MENU_MESSAGE + 1, 0, 0)) {
|
||||
textEvents.pop_back();
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
if (data->dwData != WINE_ACCESS_MENU_TAG || data->cbData != sizeof(WineAccessMenuEvent) ||
|
||||
data->lpData == nullptr || menuEvents.size() >= 32)
|
||||
return FALSE;
|
||||
@@ -385,6 +437,14 @@ LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WINE_ACCESS_MENU_MESSAGE + 1) {
|
||||
if (!textEvents.empty()) {
|
||||
const auto event = textEvents.front();
|
||||
textEvents.pop_front();
|
||||
present_text_event(event);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(window, message, wp, lp);
|
||||
}
|
||||
|
||||
@@ -411,6 +471,10 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||
event != EVENT_OBJECT_VALUECHANGE && event != EVENT_OBJECT_SELECTION &&
|
||||
event != EVENT_OBJECT_STATECHANGE)
|
||||
return;
|
||||
wchar_t className[64] = {};
|
||||
if (editReaderActive && GetClassNameW(window, className, std::size(className)) &&
|
||||
lstrcmpW(className, L"Edit") == 0)
|
||||
return;
|
||||
IAccessible *accessible = nullptr;
|
||||
VARIANT child{};
|
||||
VariantInit(&child);
|
||||
@@ -639,6 +703,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
|
||||
HWINEVENTHOOK hook = nullptr;
|
||||
HHOOK menuHook = nullptr;
|
||||
HHOOK inputHook = nullptr;
|
||||
HMODULE menuModule = nullptr;
|
||||
HWND menuBridge = nullptr;
|
||||
if (dialogReaderEnabled) {
|
||||
@@ -656,6 +721,10 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
const auto callback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessMenuHook"));
|
||||
if (callback != nullptr)
|
||||
menuHook = SetWindowsHookExW(WH_CALLWNDPROC, callback, menuModule, 0);
|
||||
const auto inputCallback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessInputHook"));
|
||||
if (inputCallback != nullptr)
|
||||
inputHook = SetWindowsHookExW(WH_GETMESSAGE, inputCallback, menuModule, 0);
|
||||
editReaderActive = menuHook != nullptr && inputHook != nullptr;
|
||||
}
|
||||
if (menuHook == nullptr)
|
||||
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
|
||||
@@ -670,6 +739,8 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
UnhookWinEvent(hook);
|
||||
if (menuHook != nullptr)
|
||||
UnhookWindowsHookEx(menuHook);
|
||||
if (inputHook != nullptr)
|
||||
UnhookWindowsHookEx(inputHook);
|
||||
if (menuBridge != nullptr)
|
||||
DestroyWindow(menuBridge);
|
||||
if (menuModule != nullptr)
|
||||
|
||||
@@ -9,6 +9,23 @@
|
||||
#define WINE_ACCESS_MENU_SUBMENU 2
|
||||
#define WINE_ACCESS_MENU_CHECKED 4
|
||||
#define WINE_ACCESS_MENU_DISABLED 8
|
||||
#define WINE_ACCESS_TEXT_TAG 0x57415431
|
||||
#define WINE_ACCESS_TEXT_FOCUS 1
|
||||
#define WINE_ACCESS_TEXT_SELECTED 2
|
||||
#define WINE_ACCESS_TEXT_UNSELECTED 3
|
||||
#define WINE_ACCESS_TEXT_DELETED 4
|
||||
#define WINE_ACCESS_TEXT_READ 5
|
||||
#define WINE_ACCESS_TEXT_PASSWORD 6
|
||||
|
||||
typedef struct WineAccessTextEvent {
|
||||
ULONGLONG owner;
|
||||
ULONGLONG control;
|
||||
UINT kind;
|
||||
DWORD start;
|
||||
DWORD end;
|
||||
WCHAR text[1024];
|
||||
WCHAR title[256];
|
||||
} WineAccessTextEvent;
|
||||
|
||||
typedef struct WineAccessMenuEvent {
|
||||
ULONGLONG owner;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "menu_bridge.h"
|
||||
#include "edit_hook.h"
|
||||
|
||||
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
||||
UINT used = 0;
|
||||
@@ -19,6 +20,7 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
|
||||
LPARAM lp) {
|
||||
if (code >= 0) {
|
||||
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
|
||||
if (message->message == WM_SETFOCUS) wine_access_watch_edit(message->hwnd);
|
||||
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
|
||||
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
||||
HWND owner = GetAncestor(message->hwnd, GA_ROOT);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include <windows.h>
|
||||
|
||||
static HWND editor, password;
|
||||
static int step;
|
||||
|
||||
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) {
|
||||
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;
|
||||
default:
|
||||
if (GetWindowTextLengthW(editor) != 7) ExitProcess(2);
|
||||
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;
|
||||
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(L"Edit", L"alpha beta\r\nsecond line",
|
||||
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE | ES_AUTOVSCROLL,
|
||||
10, 10, 450, 150, window, NULL, instance, NULL);
|
||||
password = CreateWindowW(L"Edit", L"private-password-fixture",
|
||||
WS_CHILD | WS_VISIBLE | ES_PASSWORD, 10, 180, 300, 30,
|
||||
window, NULL, instance, NULL);
|
||||
ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(editor);
|
||||
SendMessageW(editor, EM_SETSEL, 0, 0);
|
||||
SetTimer(window, 1, 350, NULL);
|
||||
MSG message;
|
||||
while (GetMessageW(&message, NULL, 0, 0) > 0) {
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -16,11 +16,13 @@ from gi.repository import Gio, GLib
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit"), default="menu")
|
||||
args = parser.parse_args()
|
||||
build = Path(args.build_dir).resolve()
|
||||
if not args.session:
|
||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
||||
__file__, "--session", "--build-dir", str(build)]))
|
||||
__file__, "--session", "--build-dir", str(build),
|
||||
"--fixture", args.fixture]))
|
||||
|
||||
interface = "org.stormux.Cthulhu1.Service"
|
||||
object_path = "/org/stormux/Cthulhu1/Service"
|
||||
@@ -47,7 +49,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||
fixture = Path(temporary) / "menu-fixture.exe"
|
||||
subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-mwindows",
|
||||
"-o", str(fixture), str(Path(__file__).with_name("menu_fixture.c")),
|
||||
"-o", str(fixture), str(Path(__file__).with_name(args.fixture + "_fixture.c")),
|
||||
"-luser32"], check=True)
|
||||
helper = None
|
||||
app = None
|
||||
@@ -72,6 +74,22 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
while GLib.MainContext.default().iteration(False):
|
||||
pass
|
||||
time.sleep(0.01)
|
||||
if args.fixture == "edit":
|
||||
text_events = [e for e in events if e[0].endswith(":text")]
|
||||
for event in text_events:
|
||||
print(event[2:7])
|
||||
spoken = [e[2] for e in text_events]
|
||||
for expected in ("l", "beta", "second line", "s selected", "e selected",
|
||||
"e unselected", "s unselected", "s deleted", "e deleted",
|
||||
"alpha selected", "alpha deleted", "😀"):
|
||||
assert expected in spoken, f"missing editor feedback: {expected!r}"
|
||||
assert any(e[3] == "edit" and e[4] == "alpha beta" for e in text_events), "initial line missing"
|
||||
assert any(e[3] == "read only edit" for e in text_events), "read-only focus missing"
|
||||
assert any(e[6] == "password" for e in text_events), "password role missing"
|
||||
assert not any("private-password-fixture" in str(e) for e in events), "password leaked"
|
||||
assert "😀 deleted" not in spoken, "read-only control reported deletion"
|
||||
print("PASS: editor focus, 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:
|
||||
print(event[2:7])
|
||||
|
||||
Reference in new issue
Block a user