Report foobar2000 Advanced settings check and radio states
Native tree descriptions previously omitted state-image bits. As a result, foobar2000 Advanced Preferences read setting names without their boolean values, and changing a checkbox without moving focus could go unnoticed. Include TVIS_STATEIMAGEMASK in the native item key so polling observes state changes even when Wine omits WinEvents. Report checked and not-checked states for standard checkbox-tree images while preserving selected and expanded/collapsed states. All queries remain read-only and retain the existing target-architecture buffer layouts and bounded query behavior. Foobar2000 also uses image indexes 3 and 4 for radio choices in its Advanced settings tree. Expose those entries as radio buttons with the corresponding check state. Restrict this application-defined mapping to foobar2000.exe and its Preferences category/settings control identities. Leave unrelated state images uninterpreted rather than inventing mixed checkbox states. Extend the controls fixture with checkbox changes that emit no WinEvent, custom images, and foobar-style radio transitions. Add a foobar-controls runner mode using the application's basename. Exercise identical control IDs and images under an unrelated basename to guard against applying the compatibility mapping to other programs. Account for Wine initializing checkbox images when the fixture first appears. Document support and the manual Preferences acceptance procedure. Validation: - Controls and foobar-controls fixtures passed in both 64-bit and 32-bit. - All six CTests passed: foreground focus, MSAA states, text limits, 64/32-bit review capture, and hook safety. - Existing menu and editor integrations passed, including Unicode, selection/deletion feedback, password exclusion and read-only controls. - A private copy of foobar2000 2.26.1 exposed Advanced setting labels, numeric values, checkbox Space toggles and radio states, plus Components row names and version/module descriptions through the recording reader. - Installed the verified helper alongside the Cthulhu session-bus fix; the user subsequently confirmed that the live changes look good. Recorded events alone do not establish physical Braille acceptance. No foobar2000 settings were changed in the user's live installation.
This commit is contained in:
1 parent
24c35d7954
commit
0763498472
5 files changed
+161
-8
No files matched your search
@@ -459,6 +459,12 @@ Native tab controls report their focused caption and position; tree controls
|
||||
report the selected node and its expansion state. A bounded focus poll follows
|
||||
navigation when Wine omits selection events. These queries work with 32-bit
|
||||
and 64-bit controls, including standard controls registered with `ATL:` names.
|
||||
Checkbox trees also report checked/not-checked states, including changes that
|
||||
do not move focus or emit a WinEvent. foobar2000's Advanced Preferences tree
|
||||
uses additional state images for radio choices; these report a radio-button
|
||||
role and checked/not-checked state. That compatibility path is restricted to
|
||||
its process and Preferences control identities. Other application-defined
|
||||
tree images are not treated as checkbox or radio states.
|
||||
Named generic MSAA clients no longer hide known native button/combo-box roles
|
||||
and values. Edit and combo-box labels can be visible siblings to the left,
|
||||
even when the labels were created after the fields. Application-owned MSAA
|
||||
|
||||
+35
-1
@@ -14,6 +14,29 @@ bool is_tab(HWND window) {
|
||||
return GetClassNameW(window, className, std::size(className)) &&
|
||||
wine_access_class_matches(className, L"SysTabControl32");
|
||||
}
|
||||
// foobar2000's Advanced tree extends standard checkbox images with a pair
|
||||
// of radio-button images. Restrict this application-defined mapping to it.
|
||||
bool is_foobar_settings_tree(HWND window) {
|
||||
if (GetDlgCtrlID(window) != 1181) return false;
|
||||
HWND root = GetAncestor(window, GA_ROOT);
|
||||
HWND categories = GetDlgItem(root, 1068);
|
||||
wchar_t className[128] = {};
|
||||
if (!categories || !GetClassNameW(categories, className, std::size(className)) ||
|
||||
!wine_access_class_matches(className, L"SysTreeView32")) return false;
|
||||
DWORD processId = 0;
|
||||
GetWindowThreadProcessId(window, &processId);
|
||||
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
|
||||
if (!process) return false;
|
||||
wchar_t path[1024] = {};
|
||||
DWORD length = std::size(path);
|
||||
const bool known = QueryFullProcessImageNameW(process, 0, path, &length);
|
||||
CloseHandle(process);
|
||||
if (!known) return false;
|
||||
const wchar_t *name = path;
|
||||
for (const wchar_t *at = path; *at; ++at)
|
||||
if (*at == L'\\' || *at == L'/') name = at + 1;
|
||||
return lstrcmpiW(name, L"foobar2000.exe") == 0;
|
||||
}
|
||||
struct Tab32 { uint32_t mask, state, stateMask, text; int32_t capacity; };
|
||||
struct Tab64 { uint32_t mask, state, stateMask, padding; uint64_t text; int32_t capacity; };
|
||||
struct Tree32 {
|
||||
@@ -60,7 +83,7 @@ NativeItemKey native_item_key(HWND window) {
|
||||
!query(window, TVM_GETNEXTITEM, TVGN_CARET, 0, &item)) return key;
|
||||
key.item = item;
|
||||
if (item && query(window, TVM_GETITEMSTATE, item,
|
||||
TVIS_SELECTED | TVIS_EXPANDED, &state)) key.state = state;
|
||||
TVIS_SELECTED | TVIS_EXPANDED | TVIS_STATEIMAGEMASK, &state)) key.state = state;
|
||||
}
|
||||
key.count = static_cast<int>(count);
|
||||
return key;
|
||||
@@ -109,6 +132,17 @@ NativeItem read_native_item(HWND window) {
|
||||
if (tab) { item.position = static_cast<int>(key.item); item.count = key.count; }
|
||||
else {
|
||||
item.role = "tree item";
|
||||
// Query state only; never toggle the item to discover its value.
|
||||
const DWORD stateImage = (key.state & TVIS_STATEIMAGEMASK) >> 12;
|
||||
const bool radio = (stateImage == 3 || stateImage == 4) &&
|
||||
is_foobar_settings_tree(window);
|
||||
const bool checkbox = (stateImage == 1 || stateImage == 2) &&
|
||||
(GetWindowLongPtrW(window, GWL_STYLE) & TVS_CHECKBOXES);
|
||||
if (radio || checkbox) {
|
||||
if (!item.states.empty()) item.states += ", ";
|
||||
item.states += stateImage == 2 || stateImage == 4 ? "checked" : "not checked";
|
||||
if (radio) item.role = "radio button";
|
||||
}
|
||||
int32_t children = 0;
|
||||
memcpy(&children, buffer + (wow64 ? offsetof(Tree32, children) : offsetof(Tree64, children)), sizeof(children));
|
||||
if (children != 0) {
|
||||
|
||||
@@ -793,3 +793,25 @@ and returning to the launcher after game exit. Recheck ordinary Wine Notepad
|
||||
typing, arrow navigation, menus and shutdown, including password exclusion.
|
||||
Audible speech and physical Braille require separate live checks. This change
|
||||
adds diagnostics; it does not establish a fix for a remote machine's silence.
|
||||
|
||||
## foobar2000 Preferences
|
||||
|
||||
Run `menu_integration.py --fixture controls` and `--fixture foobar-controls`
|
||||
in both architectures under Xvfb. The latter uses foobar's process basename
|
||||
and native control identities to check its radio images; the former verifies
|
||||
that identical images and IDs in an unrelated process do not acquire those
|
||||
semantics.
|
||||
The fixture checks tree unchecked/checked states, changes without focus
|
||||
movement or WinEvents, and unrelated state images that must remain ordinary
|
||||
items, including extra images in checkbox trees. Existing tab, tree expansion, button, combo and edit checks still apply.
|
||||
|
||||
With foobar2000 2.26.1, open Preferences, navigate categories, Tab through the
|
||||
page controls, and use Advanced's Filter and settings tree. Verify labels,
|
||||
values and expansion states; boolean settings must announce checked/not checked
|
||||
and Space changes. Under Display, inspect the album-art radio choices and
|
||||
confirm their checked/not-checked states and radio-button roles. Cancel after testing to discard settings changes. Also
|
||||
check menus, ordinary text editing and another Wine application. Recorded
|
||||
D-Bus events do not establish audible speech or physical Braille acceptance.
|
||||
If the helper reports that the reader service is unavailable, verify its
|
||||
session bus matches Cthulhu's, even when the application was launched from a
|
||||
different bus. The reader's helper launcher owns that environment policy.
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
|
||||
static HWND tabs, atlTabs, tree, combo, edit, button, list;
|
||||
static HWND tabs, atlTabs, tree, combo, edit, button, list, statusTree, settingsTree;
|
||||
static HTREEITEM root;
|
||||
static int step;
|
||||
|
||||
@@ -36,6 +36,58 @@ static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM
|
||||
case 15: focus_control(tree); break;
|
||||
case 16: SendMessageW(tree, TVM_DELETEITEM, 0, (LPARAM)TVI_ROOT); break;
|
||||
case 17: focus_control(list); break;
|
||||
case 18:
|
||||
SetWindowLongPtrW(tree, GWL_STYLE, GetWindowLongPtrW(tree, GWL_STYLE) | TVS_CHECKBOXES);
|
||||
SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&(TVINSERTSTRUCTW){
|
||||
.hParent = TVI_ROOT, .hInsertAfter = TVI_LAST,
|
||||
.item = {.mask = TVIF_TEXT | TVIF_STATE, .pszText = L"Enable playback β",
|
||||
.stateMask = TVIS_STATEIMAGEMASK, .state = INDEXTOSTATEIMAGEMASK(1)}});
|
||||
SendMessageW(tree, TVM_SELECTITEM, TVGN_CARET,
|
||||
SendMessageW(tree, TVM_GETNEXTITEM, TVGN_ROOT, 0));
|
||||
focus_control(tree);
|
||||
break;
|
||||
case 19: {
|
||||
TVITEMW item = {0};
|
||||
item.mask = TVIF_STATE;
|
||||
item.hItem = (HTREEITEM)SendMessageW(tree, TVM_GETNEXTITEM, TVGN_CARET, 0);
|
||||
item.stateMask = TVIS_STATEIMAGEMASK;
|
||||
item.state = INDEXTOSTATEIMAGEMASK(2);
|
||||
// Do not send a WinEvent: the focus poll must notice the new state.
|
||||
SendMessageW(tree, TVM_SETITEMW, 0, (LPARAM)&item);
|
||||
break;
|
||||
}
|
||||
case 20: {
|
||||
TVITEMW item = {0};
|
||||
item.mask = TVIF_STATE | TVIF_TEXT;
|
||||
item.pszText = L"Custom checkbox icon";
|
||||
item.hItem = (HTREEITEM)SendMessageW(tree, TVM_GETNEXTITEM, TVGN_CARET, 0);
|
||||
item.stateMask = TVIS_STATEIMAGEMASK;
|
||||
item.state = INDEXTOSTATEIMAGEMASK(3);
|
||||
SendMessageW(tree, TVM_SETITEMW, 0, (LPARAM)&item);
|
||||
break;
|
||||
}
|
||||
case 21: focus_control(statusTree); break;
|
||||
case 22: {
|
||||
TVITEMW item = {0};
|
||||
item.mask = TVIF_STATE;
|
||||
item.hItem = (HTREEITEM)SendMessageW(settingsTree, TVM_GETNEXTITEM, TVGN_CARET, 0);
|
||||
item.stateMask = TVIS_STATEIMAGEMASK;
|
||||
item.state = INDEXTOSTATEIMAGEMASK(3);
|
||||
// Wine initializes checkbox images when the window is first shown.
|
||||
// Install the application's custom image after that initialization.
|
||||
SendMessageW(settingsTree, TVM_SETITEMW, 0, (LPARAM)&item);
|
||||
focus_control(settingsTree);
|
||||
break;
|
||||
}
|
||||
case 23: {
|
||||
TVITEMW item = {0};
|
||||
item.mask = TVIF_STATE;
|
||||
item.hItem = (HTREEITEM)SendMessageW(settingsTree, TVM_GETNEXTITEM, TVGN_CARET, 0);
|
||||
item.stateMask = TVIS_STATEIMAGEMASK;
|
||||
item.state = INDEXTOSTATEIMAGEMASK(4);
|
||||
SendMessageW(settingsTree, TVM_SETITEMW, 0, (LPARAM)&item);
|
||||
break;
|
||||
}
|
||||
default: DestroyWindow(window); break;
|
||||
}
|
||||
return 0;
|
||||
@@ -76,7 +128,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
||||
tab.pszText = L"Second Playlist";
|
||||
SendMessageW(atlTabs, TCM_INSERTITEMW, 1, (LPARAM)&tab);
|
||||
tree = CreateWindowW(WC_TREEVIEWW, L"Tree1", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
10, 110, 300, 170, window, NULL, instance, NULL);
|
||||
10, 110, 300, 170, window, (HMENU)1068, instance, NULL);
|
||||
TVINSERTSTRUCTW node = {0};
|
||||
node.hParent = TVI_ROOT;
|
||||
node.hInsertAfter = TVI_LAST;
|
||||
@@ -105,6 +157,23 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
||||
390, 220, 200, 80, window, NULL, instance, NULL);
|
||||
SendMessageW(list, LB_ADDSTRING, 0, (LPARAM)L"Rock & roll && blues");
|
||||
SendMessageW(list, LB_SETCURSEL, 0, 0);
|
||||
statusTree = CreateWindowW(WC_TREEVIEWW, L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
|
||||
500, 310, 170, 60, window, NULL, instance, NULL);
|
||||
node.hParent = TVI_ROOT;
|
||||
node.item.mask = TVIF_TEXT | TVIF_STATE;
|
||||
node.item.pszText = L"Status image";
|
||||
node.item.stateMask = TVIS_STATEIMAGEMASK;
|
||||
node.item.state = INDEXTOSTATEIMAGEMASK(2);
|
||||
HTREEITEM status = (HTREEITEM)SendMessageW(statusTree, TVM_INSERTITEMW, 0, (LPARAM)&node);
|
||||
SendMessageW(statusTree, TVM_SELECTITEM, TVGN_CARET, (LPARAM)status);
|
||||
// Reproduce foobar's native control identities and application-defined
|
||||
// radio images. The runner tests this under matching and unrelated names.
|
||||
settingsTree = CreateWindowW(WC_TREEVIEWW, L"", WS_CHILD | WS_VISIBLE | WS_TABSTOP | TVS_CHECKBOXES,
|
||||
500, 370, 170, 40, window, (HMENU)1181, instance, NULL);
|
||||
node.item.pszText = L"Radio option";
|
||||
node.item.state = INDEXTOSTATEIMAGEMASK(3);
|
||||
HTREEITEM radio = (HTREEITEM)SendMessageW(settingsTree, TVM_INSERTITEMW, 0, (LPARAM)&node);
|
||||
SendMessageW(settingsTree, TVM_SELECTITEM, TVGN_CARET, (LPARAM)radio);
|
||||
ShowWindow(window, SW_SHOW);
|
||||
SetForegroundWindow(window);
|
||||
focus_control(tabs);
|
||||
|
||||
@@ -20,7 +20,7 @@ parser.add_argument("--build-dir", default="build")
|
||||
parser.add_argument("--session", action="store_true")
|
||||
parser.add_argument("--reader", choices=("cthulhu", "orca"))
|
||||
parser.add_argument("--extension-endpoint", action="store_true")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit", "hooks", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave", "bootstrapper"), default="menu")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit", "hooks", "notepad", "list", "listbox", "delphi", "winforms", "controls", "foobar-controls", "scintilla", "goldwave", "bootstrapper"), default="menu")
|
||||
parser.add_argument("--notepadpp-exe", type=Path)
|
||||
parser.add_argument("--goldwave-exe", type=Path)
|
||||
parser.add_argument("--32-bit", dest="is32Bit", action="store_true")
|
||||
@@ -28,7 +28,7 @@ parser.add_argument("--late-helper", action="store_true")
|
||||
parser.add_argument("--restart-helper", action="store_true")
|
||||
add_test_options(parser)
|
||||
args = parser.parse_args()
|
||||
if args.is32Bit and args.fixture not in ("menu", "edit", "hooks", "winforms", "list", "listbox", "delphi", "controls", "bootstrapper"):
|
||||
if args.is32Bit and args.fixture not in ("menu", "edit", "hooks", "winforms", "list", "listbox", "delphi", "controls", "foobar-controls", "bootstrapper"):
|
||||
parser.error("32-bit checks currently require --fixture menu, edit, hooks, winforms, list, listbox, delphi, controls or bootstrapper")
|
||||
if (args.late_helper or args.restart_helper) and args.fixture not in ("edit", "winforms", "scintilla"):
|
||||
parser.error("helper lifecycle checks require --fixture edit, winforms or scintilla")
|
||||
@@ -103,9 +103,9 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
|
||||
"x" * 1100000 + "\r\n" + "x" * 1022 + "😀tail").encode("utf-8"))
|
||||
env["WINE_ACCESS_NOTEPADPP_EXE"] = "Z:" + str(appDir / args.notepadpp_exe.name).replace("/", "\\")
|
||||
env["WINE_ACCESS_SCINTILLA_DOCUMENT"] = "Z:" + str(document).replace("/", "\\")
|
||||
fixture = Path(temporary) / "menu-fixture.exe"
|
||||
fixture = Path(temporary) / ("foobar2000.exe" if args.fixture == "foobar-controls" else "menu-fixture.exe")
|
||||
compiler = "i686-w64-mingw32-gcc" if args.is32Bit or args.fixture == "goldwave" else "x86_64-w64-mingw32-gcc"
|
||||
sourceName = "edit" if args.fixture == "winforms" else args.fixture
|
||||
sourceName = "edit" if args.fixture == "winforms" else "controls" if args.fixture == "foobar-controls" else args.fixture
|
||||
if args.fixture == "delphi":
|
||||
sourceName = "list"
|
||||
isBootstrapper = args.fixture == "bootstrapper"
|
||||
@@ -219,7 +219,7 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
|
||||
assert len(values) >= 3 and len(set(values)) >= 2, f"GoldWave combo arrow feedback missing: {values!r}"
|
||||
print("PASS: actual 32-bit GoldWave menus and file-type combo arrows without tabbing away")
|
||||
sys.exit(0)
|
||||
if args.fixture == "controls":
|
||||
if args.fixture in ("controls", "foobar-controls"):
|
||||
for event in events:
|
||||
print(event[1:9])
|
||||
for caption in ("Albums & artists β", "Details", "Default Playlist", "Second Playlist"):
|
||||
@@ -246,6 +246,28 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
|
||||
for e in events), "empty tabs retained stale caption"
|
||||
assert any(e[2] == "" and e[3] == "tree" for e in events), "empty tree retained stale item"
|
||||
assert not any(e[2] == "Tree1" for e in events), "generic tree name leaked"
|
||||
for state in ("not checked", "checked"):
|
||||
assert any(e[2] == "Enable playback β" and state in e[5].split(", ")
|
||||
for e in events), f"settings tree checkbox state missing: {state}"
|
||||
assert any(e[2] == "Enable playback β" and e[1] == "selection"
|
||||
and "checked" in e[5].split(", ") for e in events), "tree toggle without focus change missing"
|
||||
statusEvents = [e for e in events if e[2] in ("Status image", "Custom checkbox icon")
|
||||
and e[3] == "tree item"]
|
||||
assert {e[2] for e in statusEvents} == {"Status image", "Custom checkbox icon"}, "custom state images missing"
|
||||
assert statusEvents, "non-checkbox state-image tree missing"
|
||||
assert all(not any(state in e[5].split(", ") for state in
|
||||
("checked", "not checked", "half checked"))
|
||||
for e in statusEvents), "unrelated state image announced as checkbox"
|
||||
radioEvents = [e for e in events if e[2] == "Radio option"]
|
||||
assert radioEvents, "radio-image tree missing"
|
||||
if args.fixture == "foobar-controls":
|
||||
for state in ("not checked", "checked"):
|
||||
assert any(e[3] == "radio button" and state in e[5].split(", ")
|
||||
for e in radioEvents), f"foobar radio state missing: {state}"
|
||||
else:
|
||||
assert all(e[3] == "tree item" and not any(state in e[5].split(", ")
|
||||
for state in ("checked", "not checked", "half checked"))
|
||||
for e in radioEvents), "foobar mapping affected another application"
|
||||
print("PASS: native/ATL tabs, tree selection/states/return, named button, combo and edit labels, empty controls")
|
||||
sys.exit(0)
|
||||
if args.fixture == "delphi":
|
||||
|
||||
Reference in new issue
Block a user