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}"
|
add_custom_command(OUTPUT "${menu_hook}"
|
||||||
COMMAND "${MINGW_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -shared
|
COMMAND "${MINGW_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -shared
|
||||||
-static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
-static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
||||||
-luser32
|
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c" -luser32 -lcomctl32
|
||||||
DEPENDS menu_hook.c menu_bridge.h
|
DEPENDS menu_hook.c menu_bridge.h edit_hook.c edit_hook.h
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
|
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
pkgname=wine-access-git
|
pkgname=wine-access-git
|
||||||
pkgver=r3.gf365af3
|
pkgver=r6.g0992ffe
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for Cthulhu'
|
||||||
arch=('x86_64')
|
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;
|
constexpr int DBUS_TIMEOUT_MS = 500;
|
||||||
GDBusConnection *connection = nullptr;
|
GDBusConnection *connection = nullptr;
|
||||||
std::deque<WineAccessMenuEvent> menuEvents;
|
std::deque<WineAccessMenuEvent> menuEvents;
|
||||||
|
std::deque<WineAccessTextEvent> textEvents;
|
||||||
|
bool editReaderActive = false;
|
||||||
std::string lastMenuEvent;
|
std::string lastMenuEvent;
|
||||||
ULONGLONG lastMenuOwner = 0;
|
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) {
|
LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||||
if (message == WM_COPYDATA && lp != 0) {
|
if (message == WM_COPYDATA && lp != 0) {
|
||||||
const auto *data = reinterpret_cast<const COPYDATASTRUCT *>(lp);
|
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) ||
|
if (data->dwData != WINE_ACCESS_MENU_TAG || data->cbData != sizeof(WineAccessMenuEvent) ||
|
||||||
data->lpData == nullptr || menuEvents.size() >= 32)
|
data->lpData == nullptr || menuEvents.size() >= 32)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
@@ -385,6 +437,14 @@ LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
|||||||
}
|
}
|
||||||
return 0;
|
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);
|
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_VALUECHANGE && event != EVENT_OBJECT_SELECTION &&
|
||||||
event != EVENT_OBJECT_STATECHANGE)
|
event != EVENT_OBJECT_STATECHANGE)
|
||||||
return;
|
return;
|
||||||
|
wchar_t className[64] = {};
|
||||||
|
if (editReaderActive && GetClassNameW(window, className, std::size(className)) &&
|
||||||
|
lstrcmpW(className, L"Edit") == 0)
|
||||||
|
return;
|
||||||
IAccessible *accessible = nullptr;
|
IAccessible *accessible = nullptr;
|
||||||
VARIANT child{};
|
VARIANT child{};
|
||||||
VariantInit(&child);
|
VariantInit(&child);
|
||||||
@@ -639,6 +703,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
|
|
||||||
HWINEVENTHOOK hook = nullptr;
|
HWINEVENTHOOK hook = nullptr;
|
||||||
HHOOK menuHook = nullptr;
|
HHOOK menuHook = nullptr;
|
||||||
|
HHOOK inputHook = nullptr;
|
||||||
HMODULE menuModule = nullptr;
|
HMODULE menuModule = nullptr;
|
||||||
HWND menuBridge = nullptr;
|
HWND menuBridge = nullptr;
|
||||||
if (dialogReaderEnabled) {
|
if (dialogReaderEnabled) {
|
||||||
@@ -656,6 +721,10 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|||||||
const auto callback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessMenuHook"));
|
const auto callback = std::bit_cast<HOOKPROC>(GetProcAddress(menuModule, "WineAccessMenuHook"));
|
||||||
if (callback != nullptr)
|
if (callback != nullptr)
|
||||||
menuHook = SetWindowsHookExW(WH_CALLWNDPROC, callback, menuModule, 0);
|
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)
|
if (menuHook == nullptr)
|
||||||
std::fprintf(stderr, "wine-access: native menu hook unavailable (error %lu)\n",
|
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);
|
UnhookWinEvent(hook);
|
||||||
if (menuHook != nullptr)
|
if (menuHook != nullptr)
|
||||||
UnhookWindowsHookEx(menuHook);
|
UnhookWindowsHookEx(menuHook);
|
||||||
|
if (inputHook != nullptr)
|
||||||
|
UnhookWindowsHookEx(inputHook);
|
||||||
if (menuBridge != nullptr)
|
if (menuBridge != nullptr)
|
||||||
DestroyWindow(menuBridge);
|
DestroyWindow(menuBridge);
|
||||||
if (menuModule != nullptr)
|
if (menuModule != nullptr)
|
||||||
|
|||||||
@@ -9,6 +9,23 @@
|
|||||||
#define WINE_ACCESS_MENU_SUBMENU 2
|
#define WINE_ACCESS_MENU_SUBMENU 2
|
||||||
#define WINE_ACCESS_MENU_CHECKED 4
|
#define WINE_ACCESS_MENU_CHECKED 4
|
||||||
#define WINE_ACCESS_MENU_DISABLED 8
|
#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 {
|
typedef struct WineAccessMenuEvent {
|
||||||
ULONGLONG owner;
|
ULONGLONG owner;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
#include "menu_bridge.h"
|
#include "menu_bridge.h"
|
||||||
|
#include "edit_hook.h"
|
||||||
|
|
||||||
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
static void menu_text(WCHAR *output, UINT capacity, const WCHAR *input) {
|
||||||
UINT used = 0;
|
UINT used = 0;
|
||||||
@@ -19,6 +20,7 @@ __declspec(dllexport) LRESULT CALLBACK WineAccessMenuHook(int code, WPARAM wp,
|
|||||||
LPARAM lp) {
|
LPARAM lp) {
|
||||||
if (code >= 0) {
|
if (code >= 0) {
|
||||||
const CWPSTRUCT *message = (const CWPSTRUCT *)lp;
|
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) {
|
if (message->message == WM_MENUSELECT || message->message == WM_EXITMENULOOP) {
|
||||||
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
||||||
HWND owner = GetAncestor(message->hwnd, GA_ROOT);
|
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 = argparse.ArgumentParser()
|
||||||
parser.add_argument("--build-dir", default="build")
|
parser.add_argument("--build-dir", default="build")
|
||||||
parser.add_argument("--session", action="store_true")
|
parser.add_argument("--session", action="store_true")
|
||||||
|
parser.add_argument("--fixture", choices=("menu", "edit"), default="menu")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
build = Path(args.build_dir).resolve()
|
build = Path(args.build_dir).resolve()
|
||||||
if not args.session:
|
if not args.session:
|
||||||
sys.exit(subprocess.call(["dbus-run-session", "--", sys.executable,
|
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"
|
interface = "org.stormux.Cthulhu1.Service"
|
||||||
object_path = "/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")
|
WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1")
|
||||||
fixture = Path(temporary) / "menu-fixture.exe"
|
fixture = Path(temporary) / "menu-fixture.exe"
|
||||||
subprocess.run(["x86_64-w64-mingw32-gcc", "-Wall", "-Wextra", "-mwindows",
|
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)
|
"-luser32"], check=True)
|
||||||
helper = None
|
helper = None
|
||||||
app = None
|
app = None
|
||||||
@@ -72,6 +74,22 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
|||||||
while GLib.MainContext.default().iteration(False):
|
while GLib.MainContext.default().iteration(False):
|
||||||
pass
|
pass
|
||||||
time.sleep(0.01)
|
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]]
|
menus = [event for event in events if ":menu:" in event[0]]
|
||||||
for event in menus:
|
for event in menus:
|
||||||
print(event[2:7])
|
print(event[2:7])
|
||||||
|
|||||||
Reference in new issue
Block a user