Code optimization and bug fixes.
This commit is contained in:
1 parent
4c47cc5fc4
commit
b16c6968ae
11 files changed
+121
-25
No files matched your search
@@ -149,7 +149,8 @@ selected object and interface and checks the required method signatures for
|
||||
the enabled capabilities. A missing extension or incompatible interface
|
||||
causes startup to fail with a diagnostic. Controller support requires
|
||||
`SpeakText`, `CancelSpeech`, `BrailleMessage` and `SpeakSsml`; control reading
|
||||
requires `PresentAccessibleEvent`. Review methods remain optional. The
|
||||
requires `PresentAccessibleEvent`, plus `SpeakText` and `CancelSpeech` for UIA
|
||||
notifications even with `--no-controller`. Review methods remain optional. The
|
||||
interface must be published before launching the helper. This check runs at
|
||||
startup; later request failures still follow the ordinary error handling.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||
pkgname=wine-access-git
|
||||
pkgver=r37.g417ee40
|
||||
pkgver=r40.g4c47cc5
|
||||
pkgrel=1
|
||||
pkgdesc='Wine accessibility helper with MSAA, NVDA Controller, and Tolk compatibility for screen readers'
|
||||
arch=('x86_64')
|
||||
@@ -32,7 +32,8 @@ build() {
|
||||
-DCMAKE_CXX_FLAGS="$CXXFLAGS" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBEXECDIR=libexec
|
||||
-DCMAKE_INSTALL_LIBEXECDIR=libexec \
|
||||
-DWINE_ACCESS_INSTALL_DIR:PATH=libexec/wine-access
|
||||
cmake --build "$srcdir/build"
|
||||
}
|
||||
|
||||
|
||||
+28
-10
@@ -105,7 +105,12 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
||||
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;
|
||||
// GetMessage can dispatch cross-thread sends before retrieving another
|
||||
// queued message. Those replacements are not part of the UI thread's
|
||||
// pretranslated history key, even when a history boundary left it pending.
|
||||
BOOL remote_replacement = message == WM_SETTEXT && InSendMessageEx(NULL) != ISMEX_NOSEND;
|
||||
if (navigation || focus || message == WM_KILLFOCUS || remote_replacement)
|
||||
state->history_key = state->history_replaced = FALSE;
|
||||
BOOL history_replacement = message == WM_SETTEXT && state->history_key;
|
||||
if (!navigation && !changing && !focus && !selected && message != EM_SETPASSWORDCHAR)
|
||||
return DefSubclassProc(window, message, wp, lp);
|
||||
@@ -243,7 +248,8 @@ static LRESULT CALLBACK edit_proc(HWND window, UINT message, WPARAM wp, LPARAM l
|
||||
return result;
|
||||
}
|
||||
|
||||
BOOL wine_access_watch_edit(HWND window) {
|
||||
static BOOL watch_edit(HWND window, EditState **edit_state) {
|
||||
if (edit_state) *edit_state = NULL;
|
||||
WCHAR class_name[64];
|
||||
if (!window || !GetClassNameW(window, class_name, 64)) return FALSE;
|
||||
#ifdef _WIN64
|
||||
@@ -253,8 +259,12 @@ BOOL wine_access_watch_edit(HWND window) {
|
||||
if (!wine_access_class_matches(class_name, L"Edit")) return FALSE;
|
||||
// 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 FALSE;
|
||||
EditState *state = calloc(1, sizeof(*state));
|
||||
EditState *state = (EditState *)GetPropW(window, EDIT_STATE_PROPERTY);
|
||||
if (state) {
|
||||
if (edit_state) *edit_state = state;
|
||||
return FALSE;
|
||||
}
|
||||
state = calloc(1, sizeof(*state));
|
||||
if (!state) return FALSE;
|
||||
if (!SetWindowSubclass(window, edit_proc, EDIT_SUBCLASS_ID, (DWORD_PTR)state)) {
|
||||
free(state); return FALSE;
|
||||
@@ -268,20 +278,28 @@ BOOL wine_access_watch_edit(HWND window) {
|
||||
HMODULE module;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
|
||||
(LPCWSTR)(UINT_PTR)edit_proc, &module);
|
||||
if (edit_state) *edit_state = state;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL wine_access_watch_edit(HWND window) {
|
||||
return watch_edit(window, NULL);
|
||||
}
|
||||
|
||||
__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) {
|
||||
if (code >= 0 && wp == PM_REMOVE) {
|
||||
wine_access_watch_notifications();
|
||||
HWND focus = GetFocus();
|
||||
if (wine_access_watch_edit(focus))
|
||||
EditState *state;
|
||||
if (watch_edit(focus, &state)) {
|
||||
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) {
|
||||
// The synchronous callback can destroy the control. Revalidate only
|
||||
// on initial attachment, not for every message of an existing editor.
|
||||
watch_edit(focus, &state);
|
||||
}
|
||||
// watch_edit returns typed state only for Edit, never for Scintilla's
|
||||
// different structure stored under the same attachment property.
|
||||
if (state && !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.
|
||||
|
||||
@@ -176,8 +176,9 @@ bool reader_supports_protocol(bool controllerEnabled, bool dialogReaderEnabled)
|
||||
bool required;
|
||||
};
|
||||
const RequiredMethod methods[] = {
|
||||
{"SpeakText", "sb", "b", controllerEnabled},
|
||||
{"CancelSpeech", "", "b", controllerEnabled},
|
||||
// UIA notifications use speech directly even without the RPC controller.
|
||||
{"SpeakText", "sb", "b", controllerEnabled || dialogReaderEnabled},
|
||||
{"CancelSpeech", "", "b", controllerEnabled || dialogReaderEnabled},
|
||||
{"BrailleMessage", "s", "b", controllerEnabled},
|
||||
{"SpeakSsml", "ss", "i", controllerEnabled},
|
||||
{"PresentAccessibleEvent", "sssssssiis", "b", dialogReaderEnabled}};
|
||||
|
||||
@@ -104,6 +104,8 @@ invalid reader values, and startup refusal when only the unselected service
|
||||
exists. Both service names are owned while the selected reader handles requests.
|
||||
It also rejects invalid endpoint overrides, missing objects/interfaces,
|
||||
missing required methods and incorrect input/output signatures.
|
||||
UIA speech and cancellation methods are checked even with the RPC controller
|
||||
disabled; missing methods and incompatible input/output signatures must fail startup.
|
||||
The menu/editor and review tests also accept `--reader cthulhu`; omit the option
|
||||
to check existing default behavior. Review checks reject callers even when they
|
||||
own the unselected service, then replace the selected service owner and verify
|
||||
@@ -308,6 +310,9 @@ input rejected at the text limit. Command-history checks consume queued Up/Down
|
||||
before dispatch, then replace the text and move the caret as MUSHclient does.
|
||||
They require ordered, single announcements for equal-length and rapid recalls
|
||||
and empty input, with no password, background or unrelated programmatic output.
|
||||
An unchanged history boundary is followed by a worker-thread text replacement
|
||||
before another queued message is removed. That replacement must stay silent,
|
||||
and subsequent navigation must read from the updated text snapshot.
|
||||
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
|
||||
|
||||
@@ -29,12 +29,18 @@ objectPath = "/" + busName.replace(".", "/") + ("/WineAccess" if args.extension_
|
||||
endpointArguments = ["--reader-path", objectPath, "--reader-interface", interface] if args.extension_endpoint else []
|
||||
methods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
|
||||
"BrailleMessage": ("s", "b"), "SpeakSsml": ("ss", "i")}
|
||||
xml = '<node><interface name="' + interface + '">'
|
||||
for method, (inputs, output) in methods.items():
|
||||
|
||||
|
||||
def interface_xml(methods):
|
||||
xml = '<node><interface name="' + interface + '">'
|
||||
for method, (inputs, output) in methods.items():
|
||||
xml += '<method name="' + method + '">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
||||
xml += '<arg type="' + output + '" direction="out"/></method>'
|
||||
xml += '</interface></node>'
|
||||
return xml + '</interface></node>'
|
||||
|
||||
|
||||
xml = interface_xml(methods)
|
||||
events = []
|
||||
|
||||
|
||||
@@ -125,6 +131,24 @@ with tempfile.TemporaryDirectory(prefix="wine-access-controller-") as temporary:
|
||||
broken = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(brokenXml).interfaces[0], record, None, None)
|
||||
assert "reader method" in reject(helperCommand)
|
||||
connection.unregister_object(broken)
|
||||
# UIA still uses direct speech when the NVDA RPC controller is disabled.
|
||||
dialogMethods = {"SpeakText": ("sb", "b"), "CancelSpeech": ("", "b"),
|
||||
"PresentAccessibleEvent": ("sssssssiis", "b")}
|
||||
dialogCommand = ["--no-controller" if part == "--no-dialog-reader" else part
|
||||
for part in helperCommand]
|
||||
for method in ("SpeakText", "CancelSpeech"):
|
||||
for signature in (None, ("s", "b"), (dialogMethods[method][0], "i")):
|
||||
brokenMethods = dialogMethods.copy()
|
||||
if signature is None:
|
||||
del brokenMethods[method]
|
||||
else:
|
||||
brokenMethods[method] = signature
|
||||
broken = connection.register_object(objectPath,
|
||||
Gio.DBusNodeInfo.new_for_xml(interface_xml(brokenMethods)).interfaces[0],
|
||||
record, None, None)
|
||||
assert "reader method " + method in reject(dialogCommand)
|
||||
connection.unregister_object(broken)
|
||||
print("PASS: UIA speech methods required without the controller")
|
||||
registration = connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None)
|
||||
# Controller-only interfaces must not enable control reading.
|
||||
assert "PresentAccessibleEvent" in reject([part for part in helperCommand if part != "--no-dialog-reader"])
|
||||
|
||||
+32
-2
@@ -22,6 +22,29 @@ 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) {
|
||||
@@ -29,7 +52,10 @@ static BOOL history_input(const MSG *message) {
|
||||
(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;
|
||||
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);
|
||||
@@ -239,10 +265,14 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
||||
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 51:
|
||||
case 52:
|
||||
SetFocus(password);
|
||||
SendMessageW(history, EM_SETPASSWORDCHAR, 0, 0);
|
||||
PostMessageW(history, WM_KEYDOWN, VK_UP, 1); // background recall
|
||||
|
||||
@@ -35,7 +35,12 @@ interface = "org.gnome.Orca1.Service" if args.reader == "orca" else "org.stormux
|
||||
objectPath = "/" + interface.replace(".", "/")
|
||||
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in ["s"] * 7 + ["i", "i", "s"])
|
||||
xml += '<arg type="b" direction="out"/></method></interface></node>'
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
|
||||
xml += '<method name="' + method + '">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
xml += '</interface></node>'
|
||||
events = []
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,12 @@ if args.extension_endpoint:
|
||||
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
||||
for index, kind in enumerate(["s"] * 7 + ["i", "i", "s"]):
|
||||
xml += f'<arg name="arg{index}" type="{kind}" direction="in"/>'
|
||||
xml += '<arg type="b" direction="out"/></method></interface></node>'
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
|
||||
xml += '<method name="' + method + '">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
xml += '</interface></node>'
|
||||
events = []
|
||||
|
||||
def record(connection, sender, path, iface, method, parameters, invocation):
|
||||
@@ -317,7 +322,7 @@ with tempfile.TemporaryDirectory(prefix="wine-access-menus-") as temporary:
|
||||
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"], \
|
||||
"history north", "history south", "history north", "h", "blank", "blank", "n"], \
|
||||
f"command history missing, reordered, duplicated or leaked: {history_actions!r}"
|
||||
if args.fixture == "winforms":
|
||||
assert any(e[2] == "Email" and e[3] == "edit" and e[4] == "alpha beta"
|
||||
|
||||
@@ -31,7 +31,12 @@ interface = "org.stormux.Cthulhu1.Service"
|
||||
object_path = "/org/stormux/Cthulhu1/Service"
|
||||
xml = '<node><interface name="' + interface + '"><method name="PresentAccessibleEvent">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in ["s"] * 7 + ["i", "i", "s"])
|
||||
xml += '<arg type="b" direction="out"/></method></interface></node>'
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
for method, inputs in (("SpeakText", "sb"), ("CancelSpeech", "")):
|
||||
xml += '<method name="' + method + '">'
|
||||
xml += ''.join('<arg type="' + kind + '" direction="in"/>' for kind in inputs)
|
||||
xml += '<arg type="b" direction="out"/></method>'
|
||||
xml += '</interface></node>'
|
||||
events, held = [], []
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ capture_iface = 'org.stormux.Cthulhu1.WineReview'
|
||||
capture_path = '/org/stormux/Cthulhu1/WineReview'
|
||||
|
||||
def interface_xml(review=True):
|
||||
methods = {'PresentAccessibleEvent': ['s']*7+['i','i','s']}
|
||||
methods = {'PresentAccessibleEvent': ['s']*7+['i','i','s'],
|
||||
'SpeakText': ['s','b'], 'CancelSpeech': []}
|
||||
if review:
|
||||
methods.update(RegisterWineReviewProvider=['o','u'], PresentWineReviewFocus=['s','s','s','t','s','s','b','(iiii)'])
|
||||
return '<node><interface name="'+iface+'">'+''.join(
|
||||
|
||||
Reference in new issue
Block a user