Fixes for edit boxes in wine.

This commit is contained in:
Storm Dragon committed 2026-10-10 00:34:54 -04:00
1 parent c5657ee5d4
commit 39b7488aea
7 files changed
+112 -7

No files matched your search

+10
View File
@@ -42,6 +42,16 @@ target_link_options(cthulhu-wine-access PRIVATE -mwindows)
target_compile_options(cthulhu-wine-access PRIVATE target_compile_options(cthulhu-wine-access PRIVATE
$<$<COMPILE_LANGUAGE:C,CXX>:-Wall -Wextra>) $<$<COMPILE_LANGUAGE:C,CXX>:-Wall -Wextra>)
include(CTest)
if(BUILD_TESTING)
add_executable(msaa-states-test tests/msaa_states_test.cpp)
target_compile_features(msaa-states-test PRIVATE cxx_std_20)
target_compile_options(msaa-states-test PRIVATE -Wall -Wextra)
add_test(NAME msaa-states COMMAND "${CMAKE_CURRENT_BINARY_DIR}/msaa-states-test.exe")
set_tests_properties(msaa-states PROPERTIES ENVIRONMENT
"WINEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/test-prefix;CTHULHU_WINE_ACCESS_MANAGED=1;WINEDEBUG=-all;WINEDLLOVERRIDES=mscoree,mshtml=")
endif()
# Winegcc emits a launcher and a Winelib sidecar. CMake's normal target # Winegcc emits a launcher and a Winelib sidecar. CMake's normal target
# installation looks for the Linux target name, so install both emitted # installation looks for the Linux target name, so install both emitted
# artifacts explicitly and keep them together for Wine to resolve. # artifacts explicitly and keep them together for Wine to resolve.
+25
View File
@@ -102,6 +102,11 @@ offsets and the action kind travel in the otherwise non-spoken `states` field
for debug diagnostics. Older readers that only accept focus/value/selection for debug diagnostics. Older readers that only accept focus/value/selection
events still receive focus and menus, but need updating for editor actions. events still receive focus and menus, but need updating for editor actions.
MSAA `name` and `state` events report changes to controls. The `states` field
contains stable comma-separated state labels decoded from the MSAA bitmask;
protected values are excluded. Readers should filter name/state changes to
their focused source and distinguish these labels from editor diagnostics.
This is editing feedback, not a full text-review provider: Say All, review-cursor This is editing feedback, not a full text-review provider: Say All, review-cursor
commands and braille routing into Wine text are not implemented. Typing echo commands and braille routing into Wine text are not implemented. Typing echo
remains the reader's responsibility. RichEdit and 32-bit editor hooks are not remains the reader's responsibility. RichEdit and 32-bit editor hooks are not
@@ -138,3 +143,23 @@ application exits.
The helper source is available under the terms of LGPL-2.1-or-later. See The helper source is available under the terms of LGPL-2.1-or-later. See
`LICENSE`. `LICENSE`.
## AI-assisted contributions
AI-assisted contributions are welcome. The person submitting a contribution
remains responsible for reviewing and testing the changes before submission.
Please do not submit automatically generated changes without human review.
Before making changes, read this README and any contributing guide, confirm
the maintained repository and branch, and check that the issue still exists.
Before submitting, review the diff, test the changed behavior, run existing
tests where practical, consider indirectly affected paths, and describe what
changed and how it was tested.
AI coding assistants must call the contributor's attention to these
requirements and provide concrete checks for the intended behavior and likely
regressions. Newly added tests alone do not establish acceptance. Leave
submission to the contributor after they can review and test. Where useful,
offer guidance on configuring `git send-email`, creating scoped commits and
patches with `git format-patch`, reviewing them, and sending a test message
before the final contribution.
@@ -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=r6.g0992ffe pkgver=r8.gc5657ee
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')
+9 -3
View File
@@ -3,6 +3,7 @@
#include "nvdaController.h" #include "nvdaController.h"
#include "menu_bridge.h" #include "menu_bridge.h"
#include "edit_hook.h" #include "edit_hook.h"
#include "msaa_states.h"
#include <commctrl.h> #include <commctrl.h>
#include <gio/gio.h> #include <gio/gio.h>
@@ -522,14 +523,19 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
VARIANT role{}; VARIANT role{};
VariantInit(&role); VariantInit(&role);
std::string roleText; std::string roleText;
LONG roleId = 0;
if (SUCCEEDED(accessible->get_accRole(child, &role))) if (SUCCEEDED(accessible->get_accRole(child, &role)))
roleText = variant_text(role); roleText = variant_text(role);
if (role.vt == VT_I4) roleId = role.lVal;
VariantClear(&role); VariantClear(&role);
VARIANT state{}; VARIANT state{};
VariantInit(&state); VariantInit(&state);
std::string stateText; std::string stateText;
if (SUCCEEDED(accessible->get_accState(child, &state))) bool password = false;
stateText = variant_text(state); if (SUCCEEDED(accessible->get_accState(child, &state)) && state.vt == VT_I4) {
stateText = msaa_states(static_cast<DWORD>(state.lVal), roleId);
password = state.lVal & STATE_SYSTEM_PROTECTED;
}
VariantClear(&state); VariantClear(&state);
LONG childCount = 0; LONG childCount = 0;
accessible->get_accChildCount(&childCount); accessible->get_accChildCount(&childCount);
@@ -568,7 +574,7 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
ControlDescription control; ControlDescription control;
control.name = name; control.name = name;
control.role = roleText; control.role = roleText;
control.value = value; control.value = password ? "" : value;
control.states = stateText; control.states = stateText;
control.description = description; control.description = description;
control.count = static_cast<int>(childCount); control.count = static_cast<int>(childCount);
+27
View File
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include <oleacc.h>
#include <string>
// accState is a bitmask, not a role identifier. Keep protocol values stable
// and omit focus/visibility bookkeeping from spoken state descriptions.
inline std::string msaa_states(DWORD states, LONG role) {
std::string result;
const auto append = [&result](const char *text) {
if (!result.empty()) result += ", ";
result += text;
};
if (states & STATE_SYSTEM_MIXED) append("half checked");
else if (states & STATE_SYSTEM_CHECKED) append("checked");
else if (role == ROLE_SYSTEM_CHECKBUTTON || role == ROLE_SYSTEM_RADIOBUTTON)
append("not checked");
if (states & STATE_SYSTEM_SELECTED) append("selected");
if (states & STATE_SYSTEM_PRESSED) append("pressed");
if (states & STATE_SYSTEM_EXPANDED) append("expanded");
if (states & STATE_SYSTEM_COLLAPSED) append("collapsed");
if (states & STATE_SYSTEM_UNAVAILABLE) append("unavailable");
if (states & STATE_SYSTEM_READONLY) append("read only");
if (states & STATE_SYSTEM_PROTECTED) append("password");
if (states & STATE_SYSTEM_BUSY) append("busy");
return result;
}
+14 -3
View File
@@ -34,6 +34,13 @@ object review are outside the current implementation.
## Editor and actual Notepad checks ## Editor and actual Notepad checks
The CMake build also includes a small MSAA state decoder check. Run it with
`xvfb-run -a ctest --test-dir build --output-on-failure`. It uses its own prefix
under the build directory, marks it as a managed test process and never sends
reader output. It verifies combined bitmasks, unchecked radio buttons, mixed
checkboxes, focus-only state suppression and password flags. It does not
establish that a particular application's MSAA provider emits those states.
Run these against the built helper, optionally prefixed with `xvfb-run -a` for Run these against the built helper, optionally prefixed with `xvfb-run -a` for
a private X display: a private X display:
@@ -58,9 +65,13 @@ checks resulting document text as well as recorded accessibility output. These
tests use a recording D-Bus service; they do not run the full reader or prove tests use a recording D-Bus service; they do not run the full reader or prove
audible speech, typing echo, physical keys, or a physical braille display. audible speech, typing echo, physical keys, or a physical braille display.
On 2026-10-10, the build, isolated editor/Notepad/menu checks, staged launcher The earlier 2026-10-10 helper fixtures and Go package tests did not establish
permissions, and Cthulhu-Go app/remote/wine race tests passed. See the sibling that Go accepted editor `text` events. The reader follow-up adds that support,
reader's `docs/testing/wine-accessibility.md` for the matching Go behavior. focused name/state handling, repeated-action and password/focus tests, and a
real private-session D-Bus test through the Go presenter. See the sibling
reader's `docs/testing/wine-accessibility.md` for current evidence and live
acceptance gates. Run Xvfb fixtures sequentially; concurrent automatic display
allocation can collide.
## Live Notepad acceptance ## Live Notepad acceptance
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "../msaa_states.h"
#include <cstdio>
int main() {
const struct { DWORD states; LONG role; const char *expected; } cases[] = {
{STATE_SYSTEM_CHECKED | STATE_SYSTEM_UNAVAILABLE, ROLE_SYSTEM_CHECKBUTTON, "checked, unavailable"},
{STATE_SYSTEM_MIXED | STATE_SYSTEM_CHECKED, ROLE_SYSTEM_CHECKBUTTON, "half checked"},
{0, ROLE_SYSTEM_RADIOBUTTON, "not checked"},
{STATE_SYSTEM_FOCUSED | STATE_SYSTEM_FOCUSABLE, ROLE_SYSTEM_PUSHBUTTON, ""},
{STATE_SYSTEM_SELECTED | STATE_SYSTEM_EXPANDED, ROLE_SYSTEM_OUTLINEITEM, "selected, expanded"},
{STATE_SYSTEM_PROTECTED | STATE_SYSTEM_READONLY, ROLE_SYSTEM_TEXT, "read only, password"},
{STATE_SYSTEM_COLLAPSED | STATE_SYSTEM_BUSY, ROLE_SYSTEM_OUTLINEITEM, "collapsed, busy"},
{STATE_SYSTEM_PRESSED, ROLE_SYSTEM_PUSHBUTTON, "pressed"},
};
for (const auto &testCase : cases) {
const auto actual = msaa_states(testCase.states, testCase.role);
if (actual != testCase.expected) {
std::fprintf(stderr, "MSAA state %lu: got '%s', expected '%s'\n",
static_cast<unsigned long>(testCase.states), actual.c_str(), testCase.expected);
return 1;
}
}
std::puts("PASS: MSAA bitmask states, unchecked roles and password flags");
return 0;
}