// SPDX-License-Identifier: LGPL-2.1-or-later #include "native_items.h" #include "control_class.h" #include "native_query.h" #include #include #include #include namespace { using native_control::query; bool is_tab(HWND window) { wchar_t className[128] = {}; return GetClassNameW(window, className, std::size(className)) && wine_access_class_matches(className, L"SysTabControl32"); } 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 { uint32_t mask, item, state, stateMask, text; int32_t capacity, image, selectedImage, children; }; struct Tree64 { uint32_t mask, padding; uint64_t item; uint32_t state, stateMask; uint64_t text; int32_t capacity, image, selectedImage, children; }; static_assert(offsetof(Tab32, text) == 12 && offsetof(Tab64, text) == 16); static_assert(offsetof(Tree32, text) == 16 && offsetof(Tree64, text) == 24); constexpr size_t textOffset = 128; constexpr int textCapacity = 1024; constexpr size_t bufferSize = textOffset + textCapacity * sizeof(wchar_t); } bool is_native_item_control(HWND window) { wchar_t className[128] = {}; return GetClassNameW(window, className, std::size(className)) && (wine_access_class_matches(className, L"SysTabControl32") || wine_access_class_matches(className, L"SysTreeView32")); } NativeItemKey native_item_key(HWND window) { NativeItemKey key; DWORD_PTR count = 0, item = 0, state = 0; if (is_tab(window)) { if (!query(window, TCM_GETITEMCOUNT, 0, 0, &count) || count == 0) return key; // Keyboard focus can move before selection in button-style tab controls. if (!query(window, TCM_GETCURFOCUS, 0, 0, &item) || static_cast(item) < 0) if (!query(window, TCM_GETCURSEL, 0, 0, &item)) return key; if (item >= count) return key; key.item = item + 1; // Zero means no item for both control types. DWORD_PTR selected = 0; if (query(window, TCM_GETCURSEL, 0, 0, &selected) && selected == item) key.state = TVIS_SELECTED; } else { if (!query(window, TVM_GETCOUNT, 0, 0, &count) || !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; } key.count = static_cast(count); return key; } NativeItem read_native_item(HWND window) { NativeItem item; const bool tab = is_tab(window); item.role = tab ? "page tab" : "tree"; const auto key = native_item_key(window); item.key = key; if (!key.item) return item; native_control::RemoteBuffer remote(window, bufferSize); if (!remote) return item; const bool wow64 = remote.is32Bit(); BYTE buffer[bufferSize] = {}; const auto address = remote.address(textOffset); if (tab && wow64) { const Tab32 data{TCIF_TEXT, 0, 0, static_cast(address), textCapacity}; memcpy(buffer, &data, sizeof(data)); } else if (tab) { const Tab64 data{TCIF_TEXT, 0, 0, 0, address, textCapacity}; memcpy(buffer, &data, sizeof(data)); } else if (wow64) { const Tree32 data{TVIF_TEXT | TVIF_CHILDREN, static_cast(key.item), 0, 0, static_cast(address), textCapacity, 0, 0, 0}; memcpy(buffer, &data, sizeof(data)); } else { const Tree64 data{TVIF_TEXT | TVIF_CHILDREN, 0, key.item, 0, 0, address, textCapacity, 0, 0, 0}; memcpy(buffer, &data, sizeof(data)); } DWORD_PTR result = 0; if (remote.write(buffer, sizeof(buffer)) && query(window, tab ? TCM_GETITEMW : TVM_GETITEMW, tab ? key.item - 1 : 0, static_cast(remote.address()), &result) && result && remote.read(buffer, sizeof(buffer))) { wchar_t text[textCapacity] = {}; memcpy(text, buffer + textOffset, sizeof(text)); text[textCapacity - 1] = L'\0'; char utf8[textCapacity * 4] = {}; if (WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), nullptr, nullptr)) item.text = utf8; if (key.state & TVIS_SELECTED) item.states = "selected"; if (tab) { item.position = static_cast(key.item); item.count = key.count; } else { item.role = "tree item"; int32_t children = 0; memcpy(&children, buffer + (wow64 ? offsetof(Tree32, children) : offsetof(Tree64, children)), sizeof(children)); if (children != 0) { if (!item.states.empty()) item.states += ", "; item.states += key.state & TVIS_EXPANDED ? "expanded" : "collapsed"; } } } return item; }