Battle.net installer now working. It crashes when the login page loads, but that seems to be fixed in upcoming version and the controls did read.
This commit is contained in:
1 parent
38358ad27a
commit
e0c83859f5
13 files changed
+753
-11
No files matched your search
+7
-5
@@ -19,8 +19,9 @@ add_custom_command(OUTPUT "${menu_hook}"
|
||||
-static-libgcc -o "${menu_hook}" "${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/scintilla_hook.c" -luser32 -lcomctl32 -lgdi32
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c" -loleaut32
|
||||
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h edit_hook.c edit_hook.h control_class.h scintilla_hook.c scintilla_messages.h
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/bootstrapper_hook.c" -loleaut32
|
||||
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h bootstrapper_hook.c bootstrapper_hook.h edit_hook.c edit_hook.h control_class.h scintilla_hook.c scintilla_messages.h
|
||||
VERBATIM)
|
||||
add_custom_target(wine-access-menu ALL DEPENDS "${menu_hook}")
|
||||
|
||||
@@ -31,11 +32,12 @@ add_custom_command(OUTPUT "${menu_hook32}" "${menu_host32}"
|
||||
-static-libgcc -Wl,--kill-at -o "${menu_hook32}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/menu_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/edit_hook.c" -luser32 -lcomctl32
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c" -loleaut32
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/notification_hook.c"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/bootstrapper_hook.c" -loleaut32
|
||||
COMMAND "${MINGW32_C_COMPILER}" -std=c11 -O2 -Wall -Wextra -mwindows
|
||||
-static-libgcc -o "${menu_host32}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/menu_host32.c" -luser32
|
||||
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h menu_host32.c edit_hook.c edit_hook.h control_class.h
|
||||
DEPENDS menu_hook.c menu_bridge.h notification_hook.c notification_hook.h bootstrapper_hook.c bootstrapper_hook.h menu_host32.c edit_hook.c edit_hook.h control_class.h
|
||||
VERBATIM)
|
||||
add_custom_target(wine-access-menu32 ALL DEPENDS "${menu_hook32}" "${menu_host32}")
|
||||
|
||||
@@ -58,7 +60,7 @@ add_custom_command(
|
||||
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/nvdaController.idl"
|
||||
VERBATIM)
|
||||
|
||||
add_executable(wine-access reader_events.cpp msaa_events.cpp main.cpp foreground_focus.cpp list_view.cpp native_items.cpp review_capture.cpp review_x11.cpp review_service.cpp "${generated_server}")
|
||||
add_executable(wine-access reader_events.cpp msaa_events.cpp bootstrapper_controls.cpp main.cpp foreground_focus.cpp list_view.cpp native_items.cpp review_capture.cpp review_x11.cpp review_service.cpp "${generated_server}")
|
||||
add_dependencies(wine-access wine-access-menu wine-access-menu32 wine-access-version)
|
||||
target_compile_features(wine-access PRIVATE cxx_std_20)
|
||||
target_include_directories(wine-access PRIVATE "${generated_dir}")
|
||||
|
||||
@@ -256,6 +256,34 @@ The standard-control reader is deliberately limited to native Wine controls.
|
||||
Custom-drawn, DirectX, and game-specific accessibility APIs remain the
|
||||
responsibility of the application or its accessibility middleware.
|
||||
|
||||
Battle.net's `Blizzard Bootstrapper` wizard has a separate, guarded UI Automation
|
||||
reader. Its language page exposes no MSAA child controls. The helper reads the
|
||||
focused language selector and its current language, and the Continue button,
|
||||
without activating controls or changing focus. The provider marks the selected
|
||||
language as focused even while Continue owns Tab focus, so language choices
|
||||
are presented as the selector's value rather than independent focus events.
|
||||
Arrow changes are polled and unchanged results are suppressed. This support
|
||||
also reads the second page's startup checkbox through its native Toggle
|
||||
provider. Wine rejects the client toggle-state property query, so the native
|
||||
input hook borrows the focused provider on the application's UI thread and
|
||||
copies its checked/unchecked/mixed state. Only the foreground bootstrapper's
|
||||
direct `UiaReturnRawElementProvider` import is observed; the original call and
|
||||
result are preserved, no control is toggled, and provider references are not
|
||||
retained. State changes use the existing reader's `state` event.
|
||||
|
||||
After installing updated hooks, close and reopen the wizard as well as
|
||||
restarting the reader. The hook stays mapped for the application's lifetime.
|
||||
Later installer pages beyond these exposed controls and the Battle.net
|
||||
launcher remain unverified.
|
||||
|
||||
The UIA queries use a separate COM worker, limited to the foreground bootstrapper
|
||||
class, 64 sibling controls and a 500 ms result deadline. Wine's implemented
|
||||
extended property queries and an explicitly constructed tree walker avoid
|
||||
unimplemented convenience APIs. Protected field values and values whose
|
||||
protection is unknown are never queried. A blocked provider can stop this
|
||||
wizard's feedback until it returns, but does not block native menus, editing,
|
||||
MSAA events or helper shutdown. This is control feedback, not UIA flat review.
|
||||
|
||||
Native menu reading uses accompanying 64-bit and 32-bit Windows hook DLLs. They
|
||||
observe
|
||||
`WM_MENUSELECT` inside Wine applications and sends menu labels, displayed
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "bootstrapper_controls.h"
|
||||
#include <initguid.h>
|
||||
#include <uiautomation.h>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
template<class T> struct ComPtr {
|
||||
T *value = nullptr;
|
||||
~ComPtr() { if (value) value->Release(); }
|
||||
};
|
||||
struct Property {
|
||||
VARIANT value{};
|
||||
Property(IUIAutomationElement *element, PROPERTYID id) {
|
||||
// Wine implements Ex; GetCurrentPropertyValue and many convenience
|
||||
// getters return E_NOTIMPL in Wine 11.19.
|
||||
if (FAILED(element->GetCurrentPropertyValueEx(id, TRUE, &value))) VariantClear(&value);
|
||||
}
|
||||
~Property() { VariantClear(&value); }
|
||||
bool is_true() const { return value.vt == VT_BOOL && value.boolVal == VARIANT_TRUE; }
|
||||
bool is_false() const { return value.vt == VT_BOOL && value.boolVal == VARIANT_FALSE; }
|
||||
std::string string() const {
|
||||
if (value.vt != VT_BSTR || !value.bstrVal) return {};
|
||||
UINT length = SysStringLen(value.bstrVal);
|
||||
if (length > 1023) length = 1023;
|
||||
if (length && value.bstrVal[length - 1] >= 0xd800 &&
|
||||
value.bstrVal[length - 1] <= 0xdbff) --length;
|
||||
int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
||||
value.bstrVal, length, nullptr, 0, nullptr, nullptr);
|
||||
std::string result(size > 0 ? size : 0, '\0');
|
||||
if (size > 0) WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
||||
value.bstrVal, length, result.data(), size, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
std::string control_role(LONG type) {
|
||||
switch (type) {
|
||||
case UIA_ButtonControlTypeId: return "push button";
|
||||
case UIA_ComboBoxControlTypeId: return "combo box";
|
||||
case UIA_CheckBoxControlTypeId: return "check box";
|
||||
case UIA_EditControlTypeId: return "edit";
|
||||
case UIA_HyperlinkControlTypeId: return "link";
|
||||
case UIA_GroupControlTypeId: return "grouping";
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool is_bootstrapper_window(HWND window) {
|
||||
WCHAR className[128]{};
|
||||
return window && GetClassNameW(window, className, 128) &&
|
||||
lstrcmpW(className, L"Blizzard Bootstrapper") == 0;
|
||||
}
|
||||
|
||||
BootstrapperControl read_bootstrapper_control(HWND window, uint64_t deadline) {
|
||||
BootstrapperControl result;
|
||||
if (!is_bootstrapper_window(window) || GetForegroundWindow() != window) return result;
|
||||
result.window = window;
|
||||
result.thread = GetWindowThreadProcessId(window, &result.process);
|
||||
auto current = [&] { return GetTickCount64() < deadline &&
|
||||
GetForegroundWindow() == window && is_bootstrapper_window(window); };
|
||||
ComPtr<IUIAutomation> automation;
|
||||
if (FAILED(CoCreateInstance(CLSID_CUIAutomation, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_IUIAutomation, reinterpret_cast<void **>(&automation.value))) || !current()) return result;
|
||||
ComPtr<IUIAutomationCondition> condition;
|
||||
ComPtr<IUIAutomationTreeWalker> walker;
|
||||
ComPtr<IUIAutomationElement> root;
|
||||
if (FAILED(automation.value->CreateTrueCondition(&condition.value)) || !current() ||
|
||||
FAILED(automation.value->CreateTreeWalker(condition.value, &walker.value)) || !current() ||
|
||||
FAILED(automation.value->ElementFromHandle(window, &root.value)) || !current()) return result;
|
||||
IUIAutomationElement *child = nullptr;
|
||||
if (FAILED(walker.value->GetFirstChildElement(root.value, &child))) return result;
|
||||
std::string selectedLanguage;
|
||||
// The bootstrapper exposes its language choices as siblings. A radio choice
|
||||
// keeps HasKeyboardFocus even when Continue owns Tab focus: use the focused
|
||||
// combo/button as the owner, and a radio only as the combo's current value.
|
||||
for (unsigned count = 0; child && count < 64 && current(); ++count) {
|
||||
ComPtr<IUIAutomationElement> element{child};
|
||||
child = nullptr;
|
||||
Property focused(element.value, UIA_HasKeyboardFocusPropertyId);
|
||||
if (current() && focused.is_true()) {
|
||||
Property type(element.value, UIA_ControlTypePropertyId);
|
||||
Property offscreen(element.value, UIA_IsOffscreenPropertyId);
|
||||
if (current() && type.value.vt == VT_I4 && !offscreen.is_true()) {
|
||||
Property name(element.value, UIA_NamePropertyId);
|
||||
if (type.value.lVal == UIA_RadioButtonControlTypeId) {
|
||||
selectedLanguage = name.string();
|
||||
} else if (result.role.empty()) {
|
||||
result.role = control_role(type.value.lVal);
|
||||
if (!result.role.empty()) {
|
||||
result.name = name.string();
|
||||
// IDs are local to this window's observed sibling layout. Wine's
|
||||
// GetRuntimeId is also a stub; no UIA object reference crosses the
|
||||
// worker boundary or becomes an actionable review target.
|
||||
result.identity = ":" + std::to_string(count) + ":" + std::to_string(type.value.lVal);
|
||||
Property password(element.value, UIA_IsPasswordPropertyId);
|
||||
// Unknown protection is deliberately insufficient to read a value.
|
||||
if (password.is_false() && current()) {
|
||||
Property value(element.value, UIA_ValueValuePropertyId);
|
||||
result.value = value.string();
|
||||
} else if (password.is_true()) {
|
||||
result.states = "password";
|
||||
}
|
||||
Property enabled(element.value, UIA_IsEnabledPropertyId);
|
||||
if (enabled.is_false()) {
|
||||
if (!result.states.empty()) result.states += ", ";
|
||||
result.states += "unavailable";
|
||||
}
|
||||
if (type.value.lVal == UIA_CheckBoxControlTypeId && current()) {
|
||||
Property checked(element.value, UIA_ToggleToggleStatePropertyId);
|
||||
if (checked.value.vt == VT_I4 && checked.value.lVal >= ToggleState_Off &&
|
||||
checked.value.lVal <= ToggleState_Indeterminate) {
|
||||
if (!result.states.empty()) result.states += ", ";
|
||||
result.states += checked.value.lVal == ToggleState_On ? "checked" :
|
||||
checked.value.lVal == ToggleState_Indeterminate ? "half checked" : "not checked";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current()) walker.value->GetNextSiblingElement(element.value, &child);
|
||||
}
|
||||
// A partial traversal must not present a stale/ambiguous selection.
|
||||
bool complete = !child;
|
||||
if (child) child->Release();
|
||||
if (!complete || !current() || result.role.empty() || result.identity.empty()) return {};
|
||||
// This UIA provider describes its custom language selector as a Group.
|
||||
if (result.role == "grouping" && !selectedLanguage.empty()) result.role = "combo box";
|
||||
if (result.role == "combo box" && !selectedLanguage.empty()) result.value = selectedLanguage;
|
||||
result.available = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
struct BootstrapperControlReader::State {
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
std::atomic<bool> stopping{false};
|
||||
HWND window = nullptr;
|
||||
uint64_t generation = 0;
|
||||
bool pending = false, busy = false, ready = false;
|
||||
BootstrapperControl result;
|
||||
static DWORD WINAPI work(LPVOID pointer) {
|
||||
std::unique_ptr<std::shared_ptr<State>> retained(static_cast<std::shared_ptr<State> *>(pointer));
|
||||
auto state = *retained;
|
||||
const HRESULT com = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
for (;;) {
|
||||
HWND window;
|
||||
uint64_t generation;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state->mutex);
|
||||
state->changed.wait(lock, [&] { return state->stopping || state->pending; });
|
||||
if (state->stopping) break;
|
||||
window = state->window; generation = state->generation;
|
||||
state->pending = false; state->busy = true;
|
||||
}
|
||||
const uint64_t deadline = GetTickCount64() + 500;
|
||||
auto result = SUCCEEDED(com) ? read_bootstrapper_control(window, deadline) : BootstrapperControl{};
|
||||
result.generation = generation; result.deadline = deadline;
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->busy = false;
|
||||
if (!state->stopping && state->window == window && state->generation == generation) {
|
||||
state->result = std::move(result); state->ready = true;
|
||||
}
|
||||
}
|
||||
if (SUCCEEDED(com)) CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
BootstrapperControlReader::~BootstrapperControlReader() { stop(); }
|
||||
bool BootstrapperControlReader::start() {
|
||||
if (state_) return false;
|
||||
state_ = std::make_shared<State>();
|
||||
auto retained = new std::shared_ptr<State>(state_);
|
||||
worker_ = CreateThread(nullptr, 0, State::work, retained, 0, nullptr);
|
||||
if (!worker_) { delete retained; state_.reset(); return false; }
|
||||
return true;
|
||||
}
|
||||
void BootstrapperControlReader::observe(HWND window) {
|
||||
if (!state_) return;
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
if (state_->window != window) {
|
||||
state_->window = window; ++state_->generation;
|
||||
state_->pending = state_->ready = false;
|
||||
}
|
||||
if (window && !state_->stopping && !state_->busy && !state_->pending && !state_->ready) {
|
||||
state_->pending = true; state_->changed.notify_one();
|
||||
}
|
||||
}
|
||||
bool BootstrapperControlReader::take(BootstrapperControl &result) {
|
||||
if (!state_) return false;
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
if (!state_->ready) return false;
|
||||
result = std::move(state_->result); state_->ready = false;
|
||||
return true;
|
||||
}
|
||||
void BootstrapperControlReader::stop() {
|
||||
if (!state_) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_->mutex);
|
||||
state_->stopping = true; state_->pending = state_->ready = false;
|
||||
state_->changed.notify_one();
|
||||
}
|
||||
WaitForSingleObject(worker_, 100);
|
||||
CloseHandle(worker_); worker_ = nullptr; state_.reset();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
bool is_bootstrapper_window(HWND window);
|
||||
|
||||
struct BootstrapperControl {
|
||||
HWND window = nullptr;
|
||||
DWORD process = 0, thread = 0;
|
||||
uint64_t generation = 0, deadline = 0;
|
||||
bool available = false;
|
||||
std::string identity, name, role, value, states;
|
||||
};
|
||||
|
||||
// Caller initializes COM. Queries only copied properties, never invokes or
|
||||
// focuses a control. The deadline bounds traversal, not a blocked COM call.
|
||||
BootstrapperControl read_bootstrapper_control(HWND window, uint64_t deadline);
|
||||
|
||||
// A separate worker keeps a stalled UIA provider out of the MSAA, menu,
|
||||
// editor and controller paths. At most one request and one result are kept.
|
||||
class BootstrapperControlReader {
|
||||
public:
|
||||
~BootstrapperControlReader();
|
||||
bool start();
|
||||
void observe(HWND window);
|
||||
bool take(BootstrapperControl &result);
|
||||
void stop();
|
||||
private:
|
||||
struct State;
|
||||
std::shared_ptr<State> state_;
|
||||
HANDLE worker_ = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#define COBJMACROS
|
||||
#include "bootstrapper_hook.h"
|
||||
#include "menu_bridge.h"
|
||||
#include <initguid.h>
|
||||
#include <uiautomationcore.h>
|
||||
#include <uiautomationcoreapi.h>
|
||||
#include <uiautomationclient.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef LRESULT (WINAPI *ReturnProvider)(HWND, WPARAM, LPARAM, IRawElementProviderSimple *);
|
||||
static ReturnProvider originalReturn;
|
||||
static HWND queryWindow, queryBridge;
|
||||
static LONG querying;
|
||||
|
||||
static void read_toggle(IRawElementProviderSimple *root) {
|
||||
IRawElementProviderFragmentRoot *fragmentRoot = NULL;
|
||||
IRawElementProviderFragment *focus = NULL;
|
||||
IRawElementProviderSimple *provider = NULL;
|
||||
IUnknown *pattern = NULL;
|
||||
IToggleProvider *toggle = NULL;
|
||||
VARIANT type = {0}, name = {0};
|
||||
enum ToggleState state;
|
||||
if (FAILED(IRawElementProviderSimple_QueryInterface(root, &IID_IRawElementProviderFragmentRoot,
|
||||
(void **)&fragmentRoot)) || !fragmentRoot) goto done;
|
||||
if (FAILED(IRawElementProviderFragmentRoot_GetFocus(fragmentRoot, &focus)) || !focus) goto done;
|
||||
if (FAILED(IRawElementProviderFragment_QueryInterface(focus, &IID_IRawElementProviderSimple,
|
||||
(void **)&provider)) || !provider) goto done;
|
||||
if (FAILED(IRawElementProviderSimple_GetPropertyValue(provider, UIA_ControlTypePropertyId, &type)) ||
|
||||
type.vt != VT_I4 || type.lVal != UIA_CheckBoxControlTypeId) goto done;
|
||||
if (FAILED(IRawElementProviderSimple_GetPatternProvider(provider, UIA_TogglePatternId, &pattern)) ||
|
||||
!pattern || FAILED(IUnknown_QueryInterface(pattern, &IID_IToggleProvider, (void **)&toggle)) ||
|
||||
!toggle || FAILED(IToggleProvider_get_ToggleState(toggle, &state)) ||
|
||||
(state != ToggleState_Off && state != ToggleState_On && state != ToggleState_Indeterminate)) goto done;
|
||||
if (FAILED(IRawElementProviderSimple_GetPropertyValue(provider, UIA_NamePropertyId, &name)) ||
|
||||
name.vt != VT_BSTR || !name.bstrVal || GetForegroundWindow() != queryWindow) goto done;
|
||||
WineAccessBootstrapperState event = {0};
|
||||
event.owner = (ULONGLONG)(UINT_PTR)queryWindow;
|
||||
event.tick = GetTickCount64();
|
||||
event.state = state;
|
||||
UINT length = SysStringLen(name.bstrVal);
|
||||
if (length >= ARRAYSIZE(event.name)) goto done;
|
||||
memcpy(event.name, name.bstrVal, length * sizeof(WCHAR));
|
||||
COPYDATASTRUCT data = {WINE_ACCESS_BOOTSTRAPPER_TAG, sizeof(event), &event};
|
||||
DWORD_PTR result;
|
||||
SendMessageTimeoutW(queryBridge, WM_COPYDATA, 0, (LPARAM)&data,
|
||||
SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
|
||||
done:
|
||||
VariantClear(&type); VariantClear(&name);
|
||||
if (toggle) IToggleProvider_Release(toggle);
|
||||
if (pattern) IUnknown_Release(pattern);
|
||||
if (provider) IRawElementProviderSimple_Release(provider);
|
||||
if (focus) IRawElementProviderFragment_Release(focus);
|
||||
if (fragmentRoot) IRawElementProviderFragmentRoot_Release(fragmentRoot);
|
||||
}
|
||||
|
||||
static LRESULT WINAPI return_provider(HWND window, WPARAM wp, LPARAM lp,
|
||||
IRawElementProviderSimple *provider) {
|
||||
// Borrow the provider only during this original call, on its UI thread.
|
||||
// Never retain it, toggle a control, or change the original UIA result.
|
||||
if (provider && window == queryWindow && queryBridge && (LONG)lp == UiaRootObjectId)
|
||||
read_toggle(provider);
|
||||
return originalReturn(window, wp, lp, provider);
|
||||
}
|
||||
|
||||
static BOOL watch_import(void) {
|
||||
BYTE *base = (BYTE *)GetModuleHandleW(NULL);
|
||||
IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)base;
|
||||
if (!base || dos->e_magic != IMAGE_DOS_SIGNATURE) return FALSE;
|
||||
IMAGE_NT_HEADERS *header = (IMAGE_NT_HEADERS *)(base + dos->e_lfanew);
|
||||
if (header->Signature != IMAGE_NT_SIGNATURE) return FALSE;
|
||||
IMAGE_DATA_DIRECTORY directory = header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
|
||||
if (!directory.VirtualAddress || !directory.Size) return FALSE;
|
||||
IMAGE_IMPORT_DESCRIPTOR *imports = (IMAGE_IMPORT_DESCRIPTOR *)(base + directory.VirtualAddress);
|
||||
for (; imports->Name; ++imports) {
|
||||
if (lstrcmpiA((char *)(base + imports->Name), "uiautomationcore.dll") ||
|
||||
!imports->OriginalFirstThunk) continue;
|
||||
IMAGE_THUNK_DATA *names = (IMAGE_THUNK_DATA *)(base + imports->OriginalFirstThunk);
|
||||
IMAGE_THUNK_DATA *addresses = (IMAGE_THUNK_DATA *)(base + imports->FirstThunk);
|
||||
for (; names->u1.AddressOfData; ++names, ++addresses) {
|
||||
if (IMAGE_SNAP_BY_ORDINAL(names->u1.Ordinal)) continue;
|
||||
IMAGE_IMPORT_BY_NAME *name = (IMAGE_IMPORT_BY_NAME *)(base + names->u1.AddressOfData);
|
||||
if (strcmp((char *)name->Name, "UiaReturnRawElementProvider")) continue;
|
||||
if (addresses->u1.Function == (ULONG_PTR)return_provider) return TRUE;
|
||||
HMODULE uia = GetModuleHandleW(L"uiautomationcore.dll"), pinned = NULL;
|
||||
if (!uia) return FALSE;
|
||||
FARPROC address = GetProcAddress(uia, "UiaReturnRawElementProvider");
|
||||
if (!address || addresses->u1.Function != (ULONG_PTR)address) return FALSE;
|
||||
memcpy(&originalReturn, &address, sizeof(originalReturn));
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
|
||||
(LPCWSTR)(ULONG_PTR)return_provider, &pinned)) return FALSE;
|
||||
DWORD protection;
|
||||
if (!VirtualProtect(&addresses->u1.Function, sizeof(addresses->u1.Function), PAGE_READWRITE,
|
||||
&protection)) return FALSE;
|
||||
InterlockedExchangePointer((PVOID volatile *)&addresses->u1.Function, (PVOID)(ULONG_PTR)return_provider);
|
||||
DWORD ignored;
|
||||
VirtualProtect(&addresses->u1.Function, sizeof(addresses->u1.Function), protection, &ignored);
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void wine_access_query_bootstrapper_state(HWND window, HWND bridge) {
|
||||
WCHAR className[128];
|
||||
DWORD process = 0;
|
||||
if (!bridge || window != GetForegroundWindow() ||
|
||||
GetWindowThreadProcessId(window, &process) != GetCurrentThreadId() ||
|
||||
process != GetCurrentProcessId() || !GetClassNameW(window, className, ARRAYSIZE(className)) ||
|
||||
lstrcmpW(className, L"Blizzard Bootstrapper")) return;
|
||||
if (InterlockedCompareExchange(&querying, 1, 0)) return;
|
||||
if (watch_import()) {
|
||||
queryWindow = window; queryBridge = bridge;
|
||||
SendMessageW(window, WM_GETOBJECT, 0, UiaRootObjectId);
|
||||
queryWindow = queryBridge = NULL;
|
||||
}
|
||||
InterlockedExchange(&querying, 0);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#define BOOTSTRAPPER_STATE_MESSAGE L"WineAccessBootstrapperState"
|
||||
#define BOOTSTRAPPER_STATE_PENDING L"WineAccessBootstrapperStatePending"
|
||||
#define BOOTSTRAPPER_STATE_READY L"WineAccessBootstrapperStateReady"
|
||||
void wine_access_query_bootstrapper_state(HWND window, HWND bridge);
|
||||
+19
-1
@@ -3,6 +3,7 @@
|
||||
#include "menu_bridge.h"
|
||||
#include "control_class.h"
|
||||
#include "notification_hook.h"
|
||||
#include "bootstrapper_hook.h"
|
||||
#include <commctrl.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -288,8 +289,25 @@ BOOL wine_access_watch_edit(HWND window) {
|
||||
|
||||
__declspec(dllexport) LRESULT CALLBACK WineAccessInputHook(int code, WPARAM wp, LPARAM lp) {
|
||||
if (code >= 0 && wp == PM_REMOVE) {
|
||||
wine_access_watch_notifications();
|
||||
const MSG *bootstrapperMessage = (const MSG *)lp;
|
||||
HWND focus = GetFocus();
|
||||
WCHAR className[128];
|
||||
if (focus && GetClassNameW(focus, className, ARRAYSIZE(className)) &&
|
||||
!lstrcmpW(className, L"Blizzard Bootstrapper")) {
|
||||
HWND bridge = FindWindowW(WINE_ACCESS_MENU_CLASS, NULL);
|
||||
// Associate readiness with this reader instance, not just a DLL left
|
||||
// mapped in the application after the previous reader exited.
|
||||
if (bridge && GetPropW(focus, BOOTSTRAPPER_STATE_READY) != (HANDLE)bridge) {
|
||||
RemovePropW(focus, BOOTSTRAPPER_STATE_PENDING);
|
||||
SetPropW(focus, BOOTSTRAPPER_STATE_READY, (HANDLE)bridge);
|
||||
}
|
||||
}
|
||||
if (bootstrapperMessage->message == RegisterWindowMessageW(BOOTSTRAPPER_STATE_MESSAGE)) {
|
||||
wine_access_query_bootstrapper_state(bootstrapperMessage->hwnd,
|
||||
FindWindowW(WINE_ACCESS_MENU_CLASS, NULL));
|
||||
RemovePropW(bootstrapperMessage->hwnd, BOOTSTRAPPER_STATE_PENDING);
|
||||
}
|
||||
wine_access_watch_notifications();
|
||||
EditState *state;
|
||||
if (watch_edit(focus, &state)) {
|
||||
SendMessageW(focus, RegisterWindowMessageW(EDIT_FOCUS_MESSAGE), 0, 0);
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include "reader_endpoint.h"
|
||||
#include "msaa_events.h"
|
||||
#include "reader_events.h"
|
||||
#include "bootstrapper_controls.h"
|
||||
#include "bootstrapper_hook.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <commctrl.h>
|
||||
@@ -37,6 +39,10 @@ GDBusConnection *connection = nullptr;
|
||||
ReviewIdentityStore reviewIdentities;
|
||||
ReviewService reviewService;
|
||||
MsaaEventReader msaaEvents;
|
||||
BootstrapperControlReader bootstrapperReader;
|
||||
HWND lastBootstrapperWindow = nullptr;
|
||||
std::string lastBootstrapperIdentity, lastBootstrapperPresentation;
|
||||
WineAccessBootstrapperState bootstrapperState{};
|
||||
ReaderEventQueue readerEvents;
|
||||
uint64_t msaaGeneration = 0;
|
||||
MsaaEvent pendingMsaaFocus; // Event-thread owned; retired by focus changes, results or timeout.
|
||||
@@ -500,6 +506,54 @@ void poll_control_focus(bool refresh = false) {
|
||||
const bool focused = foreground &&
|
||||
GetGUIThreadInfo(GetWindowThreadProcessId(foreground, nullptr), &info) &&
|
||||
!(info.flags & GUI_INMENUMODE);
|
||||
HWND bootstrapper = focused && info.hwndFocus == foreground &&
|
||||
is_bootstrapper_window(foreground) ? foreground : nullptr;
|
||||
bootstrapperReader.observe(bootstrapper);
|
||||
if (bootstrapper != lastBootstrapperWindow) {
|
||||
bootstrapperState = {};
|
||||
lastBootstrapperWindow = bootstrapper;
|
||||
lastBootstrapperIdentity.clear(); lastBootstrapperPresentation.clear();
|
||||
}
|
||||
// Keep one native query outstanding even if the application stops pumping
|
||||
// messages. The input hook clears the marker after the query completes.
|
||||
HWND bridge = bootstrapper ? FindWindowW(WINE_ACCESS_MENU_CLASS, nullptr) : nullptr;
|
||||
if (bootstrapper && bridge && GetPropW(bootstrapper, BOOTSTRAPPER_STATE_READY) ==
|
||||
reinterpret_cast<HANDLE>(bridge) && !GetPropW(bootstrapper, BOOTSTRAPPER_STATE_PENDING) &&
|
||||
SetPropW(bootstrapper, BOOTSTRAPPER_STATE_PENDING, reinterpret_cast<HANDLE>(1))) {
|
||||
if (!PostMessageW(bootstrapper, RegisterWindowMessageW(BOOTSTRAPPER_STATE_MESSAGE), 0, 0))
|
||||
RemovePropW(bootstrapper, BOOTSTRAPPER_STATE_PENDING);
|
||||
}
|
||||
BootstrapperControl result;
|
||||
if (bootstrapperReader.take(result) && result.available &&
|
||||
result.window == bootstrapper && GetTickCount64() < result.deadline) {
|
||||
DWORD process = 0;
|
||||
const DWORD thread = GetWindowThreadProcessId(bootstrapper, &process);
|
||||
if (thread == result.thread && process == result.process) {
|
||||
const std::string key = result.identity + "\n" + result.name + "\n" +
|
||||
result.role + "\n" + result.value + "\n" + result.states;
|
||||
ControlDescription control;
|
||||
control.name = result.name; control.role = result.role;
|
||||
control.value = result.value; control.states = result.states;
|
||||
if (control.role == "check box" && bootstrapperState.owner ==
|
||||
static_cast<ULONGLONG>(reinterpret_cast<UINT_PTR>(bootstrapper)) &&
|
||||
GetTickCount64() - bootstrapperState.tick < 500 &&
|
||||
to_utf8(bootstrapperState.name) == control.name) {
|
||||
control.states = bootstrapperState.state == 0 ? "not checked" :
|
||||
bootstrapperState.state == 1 ? "checked" : "half checked";
|
||||
for (const char *flag : {"password", "unavailable"}) {
|
||||
if (result.states.find(flag) != std::string::npos) control.states += std::string(", ") + flag;
|
||||
}
|
||||
}
|
||||
const std::string presentation = key + "\n" + control.states;
|
||||
const bool focusChanged = result.identity != lastBootstrapperIdentity;
|
||||
if (presentation != lastBootstrapperPresentation &&
|
||||
present_accessible_event(source_id(bootstrapper, "bootstrapper-uia") + result.identity,
|
||||
focusChanged ? "focus" : control.role == "check box" ? "state" : "value",
|
||||
control, window_title(bootstrapper))) {
|
||||
lastBootstrapperIdentity = result.identity; lastBootstrapperPresentation = presentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool focusedListBox = focused && is_list_box(info.hwndFocus);
|
||||
if (!focusedListBox) {
|
||||
lastListBoxControl = nullptr;
|
||||
@@ -693,6 +747,16 @@ void present_text_event(const WineAccessTextEvent &event) {
|
||||
LRESULT CALLBACK menu_bridge_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_COPYDATA && lp != 0) {
|
||||
const auto *data = reinterpret_cast<const COPYDATASTRUCT *>(lp);
|
||||
if (data->dwData == WINE_ACCESS_BOOTSTRAPPER_TAG &&
|
||||
data->cbData == sizeof(WineAccessBootstrapperState) && data->lpData) {
|
||||
const auto event = *static_cast<const WineAccessBootstrapperState *>(data->lpData);
|
||||
HWND owner = reinterpret_cast<HWND>(static_cast<UINT_PTR>(event.owner));
|
||||
if (owner != GetForegroundWindow() || !is_bootstrapper_window(owner) ||
|
||||
event.state > 2 || GetTickCount64() - event.tick >= 500) return FALSE;
|
||||
bootstrapperState = event;
|
||||
bootstrapperState.name[std::size(bootstrapperState.name) - 1] = 0;
|
||||
return TRUE;
|
||||
}
|
||||
if (data->dwData == WINE_ACCESS_NOTIFICATION_TAG &&
|
||||
data->cbData == sizeof(WineAccessNotificationEvent) &&
|
||||
data->lpData != nullptr && notificationEvents.size() < 32) {
|
||||
@@ -812,6 +876,10 @@ void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
||||
event == EVENT_OBJECT_VALUECHANGE || event == EVENT_OBJECT_SELECTION || event == EVENT_OBJECT_STATECHANGE;
|
||||
if (presentationEvent && !msaa_event_in_foreground(window)) return;
|
||||
if (event == EVENT_OBJECT_FOCUS || event == EVENT_SYSTEM_FOREGROUND) {
|
||||
bootstrapperReader.observe(nullptr);
|
||||
bootstrapperState = {};
|
||||
lastBootstrapperWindow = nullptr;
|
||||
lastBootstrapperIdentity.clear(); lastBootstrapperPresentation.clear();
|
||||
pendingMsaaFocus = {};
|
||||
++msaaGeneration;
|
||||
msaaEvents.invalidate(msaaGeneration);
|
||||
@@ -1248,6 +1316,8 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
// Native lists, tabs, trees and combo boxes can omit change WinEvents in Wine.
|
||||
// Observe only the foreground control; never read background selections.
|
||||
const UINT_PTR listTimer = dialogReaderEnabled ? SetTimer(nullptr, 0, 100, nullptr) : 0;
|
||||
if (dialogReaderEnabled && !bootstrapperReader.start())
|
||||
std::fprintf(stderr, "wine-access: bootstrapper UIA worker unavailable\n");
|
||||
if (dialogReaderEnabled && !listTimer)
|
||||
std::fprintf(stderr, "wine-access: control polling timer unavailable\n");
|
||||
// Initialization status is distinct from process creation. Hook handles and
|
||||
@@ -1282,6 +1352,7 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
||||
if (listTimer) KillTimer(nullptr, listTimer);
|
||||
readerEvents.stop();
|
||||
msaaEvents.stop();
|
||||
bootstrapperReader.stop();
|
||||
reviewService.stop();
|
||||
if (hook != nullptr)
|
||||
UnhookWinEvent(hook);
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
#define WINE_ACCESS_TEXT_SCINTILLA 1
|
||||
#define WINE_ACCESS_TEXT_READONLY 2
|
||||
#define WINE_ACCESS_NOTIFICATION_TAG 0x57414e31
|
||||
#define WINE_ACCESS_BOOTSTRAPPER_TAG 0x57414231
|
||||
|
||||
typedef struct WineAccessBootstrapperState {
|
||||
ULONGLONG owner;
|
||||
ULONGLONG tick;
|
||||
UINT state;
|
||||
WCHAR name[1024];
|
||||
} WineAccessBootstrapperState;
|
||||
|
||||
typedef struct WineAccessNotificationEvent {
|
||||
DWORD process;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include "menu_bridge.h"
|
||||
#include "bootstrapper_hook.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -61,6 +62,19 @@ int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR args, int show)
|
||||
if (hook) UnhookWindowsHookEx(hook);
|
||||
if (inputHook) UnhookWindowsHookEx(inputHook);
|
||||
hook = inputHook = NULL;
|
||||
} else {
|
||||
HWND foreground = GetForegroundWindow();
|
||||
WCHAR className[128];
|
||||
if (GetWindowThreadProcessId(foreground, NULL) == hookedThread &&
|
||||
GetClassNameW(foreground, className, ARRAYSIZE(className)) &&
|
||||
!lstrcmpW(className, L"Blizzard Bootstrapper")) {
|
||||
// A query can have passed through while this thread's hook was
|
||||
// detached. Reestablish readiness on every attachment, including
|
||||
// return from another workspace/application.
|
||||
RemovePropW(foreground, BOOTSTRAPPER_STATE_READY);
|
||||
RemovePropW(foreground, BOOTSTRAPPER_STATE_PENDING);
|
||||
PostMessageW(foreground, WM_NULL, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
DWORD result = MsgWaitForMultipleObjects(1, &parent, FALSE, 100, QS_ALLINPUT);
|
||||
|
||||
@@ -41,6 +41,53 @@ Check the runner without Wine:
|
||||
python3 tests/wine_test_environment_test.py
|
||||
```
|
||||
|
||||
## Battle.net bootstrapper controls
|
||||
|
||||
```sh
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture bootstrapper --32-bit
|
||||
xvfb-run -a python3 tests/menu_integration.py --build-dir build --fixture bootstrapper
|
||||
```
|
||||
|
||||
Run these sequentially. The fixture supplies an actual custom UIA provider
|
||||
without native child controls. A private recording reader verifies language
|
||||
value changes, Continue focus despite a radio item retaining its focus flag,
|
||||
return to the selector, duplicate suppression, background exclusion and return,
|
||||
and exclusion of protected or unknown-protection values before querying them.
|
||||
The real native input hook also reads a Toggle provider whose state is absent
|
||||
from the provider's simple properties. It checks unchecked, checked and mixed
|
||||
states, changes without focus movement, focus return and background exclusion.
|
||||
The fixture fails if the helper invokes Toggle rather than reading its state.
|
||||
The test does not use the real installer or send speech to the running reader.
|
||||
|
||||
Read-only inspection on October 16 retrieved the actual running setup's
|
||||
`Select a Language, combo box, English (United States)` and `Continue, push
|
||||
button` through the new query code during Tab navigation. This verifies the
|
||||
query path, not installed-helper speech or physical Braille.
|
||||
|
||||
A later read-only native probe on the second page retrieved the startup
|
||||
checkbox's real Toggle state while Wine's client state query returned
|
||||
`E_NOTIMPL`. The first installed build was confirmed by the user to read the
|
||||
language page and allow progress to the second screen; audible checkbox-state
|
||||
acceptance remains separate.
|
||||
|
||||
After building/installing Wine Access, restart Cthulhu and reopen the wizard
|
||||
to load the new native hook. Return to the
|
||||
wizard on workspace 2. Tab/Shift+Tab should distinguish the language selector
|
||||
from Continue. With the selector focused, Down/Up should announce the current
|
||||
language; restore the intended language afterward. Pause to check for repeated
|
||||
announcements, then switch to Linux and back to check background silence and
|
||||
focus recovery. Only activate Continue when ready to advance the installer;
|
||||
later pages and the launcher remain unverified. Check ordinary Wine Notepad
|
||||
typing/arrows, menus/Escape, native combo-box arrows and reader shutdown for
|
||||
regressions. Verify audible speech and physical Braille separately.
|
||||
|
||||
On the second page, Tab to `Launch Battle.net when you start your computer`:
|
||||
expect its current checked/unchecked state. Press Space twice and verify each
|
||||
new state immediately, leaving the original setting restored. Tab away and
|
||||
Shift+Tab back should retain the correct state. Switching away must silence
|
||||
background changes; returning should reestablish the state. Choose the desired
|
||||
startup setting before continuing installation.
|
||||
|
||||
## UIA notifications
|
||||
|
||||
UIA notification routing uses the actual native application hooks and a private
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
#include <windows.h>
|
||||
#include <uiautomation.h>
|
||||
#include <cstdio>
|
||||
|
||||
static HWND window;
|
||||
static int focusedIndex = 1, selectedIndex = 3, step = 0;
|
||||
static ToggleState checkboxState = ToggleState_Off;
|
||||
static bool checkboxEnabled = true;
|
||||
|
||||
class Provider : public IRawElementProviderSimple,
|
||||
public IRawElementProviderFragment, public IRawElementProviderFragmentRoot, public IToggleProvider {
|
||||
public:
|
||||
int index;
|
||||
explicit Provider(int value) : index(value) {}
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void **out) override {
|
||||
*out = nullptr;
|
||||
if (IsEqualIID(iid, IID_IUnknown) || IsEqualIID(iid, IID_IRawElementProviderSimple))
|
||||
*out = static_cast<IRawElementProviderSimple *>(this);
|
||||
else if (IsEqualIID(iid, IID_IRawElementProviderFragment))
|
||||
*out = static_cast<IRawElementProviderFragment *>(this);
|
||||
else if (index == 0 && IsEqualIID(iid, IID_IRawElementProviderFragmentRoot))
|
||||
*out = static_cast<IRawElementProviderFragmentRoot *>(this);
|
||||
else if (index == 7 && IsEqualIID(iid, IID_IToggleProvider))
|
||||
*out = static_cast<IToggleProvider *>(this);
|
||||
return *out ? S_OK : E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE AddRef() override { return 2; }
|
||||
ULONG STDMETHODCALLTYPE Release() override { return 1; }
|
||||
HRESULT STDMETHODCALLTYPE get_ProviderOptions(ProviderOptions *out) override {
|
||||
*out = ProviderOptions_ServerSideProvider; return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetPatternProvider(PATTERNID id, IUnknown **out) override {
|
||||
*out = index == 7 && id == UIA_TogglePatternId ? static_cast<IToggleProvider *>(this) : nullptr;
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE Toggle() override { ExitProcess(9); }
|
||||
HRESULT STDMETHODCALLTYPE get_ToggleState(ToggleState *out) override { *out = checkboxState; return S_OK; }
|
||||
HRESULT STDMETHODCALLTYPE GetPropertyValue(PROPERTYID id, VARIANT *out) override {
|
||||
VariantInit(out);
|
||||
static const WCHAR *names[] = {L"Bootstrapper fixture", L"Select a Language", L"Continue",
|
||||
L"English (United States)", L"French", L"Password", L"Unknown protection", L"Launch on startup"};
|
||||
if (id == UIA_NamePropertyId) {
|
||||
out->vt = VT_BSTR; out->bstrVal = SysAllocString(names[index]);
|
||||
} else if (id == UIA_ControlTypePropertyId) {
|
||||
out->vt = VT_I4;
|
||||
out->lVal = index == 7 ? UIA_CheckBoxControlTypeId : index == 0 ? UIA_WindowControlTypeId : index == 1 ? UIA_GroupControlTypeId :
|
||||
index == 2 ? UIA_ButtonControlTypeId : index < 5 ? UIA_RadioButtonControlTypeId : UIA_EditControlTypeId;
|
||||
} else if (id == UIA_HasKeyboardFocusPropertyId || id == UIA_IsKeyboardFocusablePropertyId ||
|
||||
id == UIA_IsOffscreenPropertyId || id == UIA_IsEnabledPropertyId ||
|
||||
id == UIA_IsPasswordPropertyId) {
|
||||
if (id == UIA_IsPasswordPropertyId && index == 6) return S_OK;
|
||||
out->vt = VT_BOOL;
|
||||
bool value = id == UIA_HasKeyboardFocusPropertyId ? (index == focusedIndex || index == selectedIndex) :
|
||||
id == UIA_IsKeyboardFocusablePropertyId ? index != 0 :
|
||||
id == UIA_IsOffscreenPropertyId ? false :
|
||||
id == UIA_IsPasswordPropertyId ? index == 5 : index != 7 || checkboxEnabled;
|
||||
out->boolVal = value ? VARIANT_TRUE : VARIANT_FALSE;
|
||||
} else if (id == UIA_ValueValuePropertyId && (index == 5 || index == 6)) {
|
||||
// Protected and unknown-protection values must never even be queried.
|
||||
ExitProcess(8);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE get_HostRawElementProvider(IRawElementProviderSimple **out) override {
|
||||
*out = nullptr; return index == 0 ? UiaHostProviderFromHwnd(window, out) : S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE Navigate(NavigateDirection direction, IRawElementProviderFragment **out) override;
|
||||
HRESULT STDMETHODCALLTYPE GetRuntimeId(SAFEARRAY **out) override {
|
||||
*out = nullptr;
|
||||
if (index == 0) return S_OK;
|
||||
*out = SafeArrayCreateVector(VT_I4, 0, 2);
|
||||
LONG position = 0, value = UiaAppendRuntimeId;
|
||||
SafeArrayPutElement(*out, &position, &value);
|
||||
position = 1; value = index; SafeArrayPutElement(*out, &position, &value); return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE get_BoundingRectangle(UiaRect *out) override {
|
||||
*out = {100, 100.0 + index * 20, 200, 20}; return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetEmbeddedFragmentRoots(SAFEARRAY **out) override { *out = nullptr; return S_OK; }
|
||||
HRESULT STDMETHODCALLTYPE SetFocus() override { return E_NOTIMPL; }
|
||||
HRESULT STDMETHODCALLTYPE get_FragmentRoot(IRawElementProviderFragmentRoot **out) override;
|
||||
HRESULT STDMETHODCALLTYPE ElementProviderFromPoint(double, double, IRawElementProviderFragment **out) override {
|
||||
*out = nullptr; return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GetFocus(IRawElementProviderFragment **out) override;
|
||||
};
|
||||
static Provider providers[] = {Provider(0), Provider(1), Provider(2), Provider(3), Provider(4), Provider(5), Provider(6), Provider(7)};
|
||||
HRESULT Provider::Navigate(NavigateDirection direction, IRawElementProviderFragment **out) {
|
||||
*out = nullptr;
|
||||
if (direction == NavigateDirection_Parent && index) *out = &providers[0];
|
||||
else if (direction == NavigateDirection_FirstChild && index == 0) *out = &providers[1];
|
||||
else if (direction == NavigateDirection_LastChild && index == 0) *out = &providers[7];
|
||||
else if (direction == NavigateDirection_NextSibling && index > 0 && index < 7) *out = &providers[index + 1];
|
||||
else if (direction == NavigateDirection_PreviousSibling && index > 1) *out = &providers[index - 1];
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT Provider::get_FragmentRoot(IRawElementProviderFragmentRoot **out) { *out = &providers[0]; return S_OK; }
|
||||
HRESULT Provider::GetFocus(IRawElementProviderFragment **out) { *out = &providers[focusedIndex]; return S_OK; }
|
||||
|
||||
static LRESULT CALLBACK window_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == WM_GETOBJECT && static_cast<LONG>(lp) == UiaRootObjectId)
|
||||
return UiaReturnRawElementProvider(hwnd, wp, lp, &providers[0]);
|
||||
if (message == WM_TIMER) {
|
||||
switch (++step) {
|
||||
case 1: break;
|
||||
case 2: selectedIndex = 4; break;
|
||||
case 3: focusedIndex = 2; break;
|
||||
case 4: focusedIndex = 1; break;
|
||||
case 5: focusedIndex = 5; break;
|
||||
case 6: focusedIndex = 6; break;
|
||||
case 7: focusedIndex = 1; selectedIndex = 3; break;
|
||||
case 8: focusedIndex = 7; break;
|
||||
case 9: checkboxState = ToggleState_On; break;
|
||||
case 10: checkboxState = ToggleState_Off; break;
|
||||
case 11: checkboxState = ToggleState_Indeterminate; checkboxEnabled = false; break;
|
||||
case 12: focusedIndex = 2; break;
|
||||
case 13: focusedIndex = 7; break;
|
||||
case 14: ShowWindow(hwnd, SW_HIDE); checkboxState = ToggleState_On; checkboxEnabled = true; break;
|
||||
case 15: ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); SetFocus(hwnd); break;
|
||||
default: DestroyWindow(hwnd); break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_DESTROY) { PostQuitMessage(0); return 0; }
|
||||
return DefWindowProcW(hwnd, message, wp, lp);
|
||||
}
|
||||
int WINAPI WinMain(HINSTANCE instance, HINSTANCE, LPSTR, int) {
|
||||
if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) return 1;
|
||||
WNDCLASSW cls{};
|
||||
cls.hInstance = instance; cls.lpfnWndProc = window_proc; cls.lpszClassName = L"Blizzard Bootstrapper";
|
||||
RegisterClassW(&cls);
|
||||
window = CreateWindowW(cls.lpszClassName, L"Bootstrapper fixture", WS_OVERLAPPEDWINDOW,
|
||||
100, 100, 400, 300, nullptr, nullptr, instance, nullptr);
|
||||
ShowWindow(window, SW_SHOW); SetForegroundWindow(window); SetFocus(window);
|
||||
SetTimer(window, 1, 800, nullptr);
|
||||
MSG message{};
|
||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
CoUninitialize();
|
||||
return 0;
|
||||
}
|
||||
@@ -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", "notepad", "list", "listbox", "delphi", "winforms", "controls", "scintilla", "goldwave"), default="menu")
|
||||
parser.add_argument("--fixture", choices=("menu", "edit", "notepad", "list", "listbox", "delphi", "winforms", "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,8 +28,8 @@ 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", "winforms", "list", "listbox", "delphi", "controls"):
|
||||
parser.error("32-bit checks currently require --fixture menu, edit, winforms, list, listbox, delphi or controls")
|
||||
if args.is32Bit and args.fixture not in ("menu", "edit", "winforms", "list", "listbox", "delphi", "controls", "bootstrapper"):
|
||||
parser.error("32-bit checks currently require --fixture menu, edit, 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")
|
||||
if args.fixture == "scintilla" and (not args.notepadpp_exe or not args.notepadpp_exe.is_file()):
|
||||
@@ -108,11 +108,16 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
|
||||
sourceName = "edit" if args.fixture == "winforms" else args.fixture
|
||||
if args.fixture == "delphi":
|
||||
sourceName = "list"
|
||||
isBootstrapper = args.fixture == "bootstrapper"
|
||||
if isBootstrapper:
|
||||
compiler = compiler.replace("gcc", "g++")
|
||||
subprocess.run([compiler, "-Wall", "-Wextra", "-mwindows",
|
||||
*(["-static"] if isBootstrapper else []),
|
||||
*(["-DWINE_ACCESS_WINFORMS_FIXTURE"] if args.fixture == "winforms" else []),
|
||||
*(["-DWINE_ACCESS_DELPHI_FIXTURE"] if args.fixture == "delphi" else []),
|
||||
"-o", str(fixture), str(Path(__file__).with_name(sourceName + "_fixture.c")),
|
||||
"-luser32", "-lcomctl32", "-loleacc", "-lole32", "-ladvapi32"], check=True)
|
||||
"-o", str(fixture), str(Path(__file__).with_name(sourceName + ("_fixture.cpp" if isBootstrapper else "_fixture.c"))),
|
||||
"-luser32", "-lcomctl32", "-loleacc", "-lole32", "-ladvapi32",
|
||||
*(["-luiautomationcore", "-loleaut32", "-luuid"] if isBootstrapper else [])], check=True)
|
||||
helper = None
|
||||
app = None
|
||||
try:
|
||||
@@ -160,6 +165,43 @@ with wine_test_directory(prefix="wine-access-menus-") as temporary:
|
||||
assert event[0] == focused, f"text sent without establishing focus: {event!r}"
|
||||
elif event[1] == "value" and event[3] == "combo box":
|
||||
assert event[0] == focused, f"combo value sent for an unfocused source: {event!r}; focus={focused}; events={events!r}"
|
||||
if args.fixture == "bootstrapper":
|
||||
uiaEvents = [e for e in events if ":bootstrapper-uia:" in e[0]]
|
||||
for event in uiaEvents:
|
||||
print(event[1:9])
|
||||
# Focus can arrive before the native hook's state reply. It must
|
||||
# then be followed by a known state for that same focused source.
|
||||
for index, event in enumerate(uiaEvents):
|
||||
if event[3] == "check box" and not event[5]:
|
||||
assert event[1] == "focus", f"lost a previously known checkbox state: {event!r}"
|
||||
assert index + 1 < len(uiaEvents) and uiaEvents[index + 1][0] == event[0] and uiaEvents[index + 1][5], \
|
||||
f"checkbox state never arrived after focus: {uiaEvents[index:]!r}"
|
||||
knownEvents = [e for e in uiaEvents if e[3] != "check box" or e[5]]
|
||||
assert [e[2:5] for e in knownEvents] == [
|
||||
("Select a Language", "combo box", "English (United States)"),
|
||||
("Select a Language", "combo box", "French"),
|
||||
("Continue", "push button", ""),
|
||||
("Select a Language", "combo box", "French"),
|
||||
("Password", "edit", ""),
|
||||
("Unknown protection", "edit", ""),
|
||||
("Select a Language", "combo box", "English (United States)"),
|
||||
("Launch on startup", "check box", ""),
|
||||
("Launch on startup", "check box", ""),
|
||||
("Launch on startup", "check box", ""),
|
||||
("Launch on startup", "check box", ""),
|
||||
("Continue", "push button", ""),
|
||||
("Launch on startup", "check box", ""),
|
||||
("Launch on startup", "check box", ""),
|
||||
], f"missing, repeated or background bootstrapper feedback: {uiaEvents!r}"
|
||||
assert [e[1] for e in knownEvents[:7]] == ["focus", "value"] + ["focus"] * 5
|
||||
checkboxEvents = [e for e in knownEvents if e[3] == "check box"]
|
||||
assert [e[1] for e in checkboxEvents[1:4]] == ["state"] * 3
|
||||
assert [e[5] for e in checkboxEvents] == [
|
||||
"not checked", "checked", "not checked", "half checked, unavailable", "half checked, unavailable", "checked"]
|
||||
assert "password" in uiaEvents[4][5], "protected control lost its state"
|
||||
assert len({e[0] for e in uiaEvents if e[3] == "combo box"}) == 1
|
||||
print("PASS: UIA selector values, native Toggle checked/unchecked/mixed, focus/return, deduplication, protected/unknown values, background exclusion")
|
||||
sys.exit(0)
|
||||
if args.fixture == "goldwave":
|
||||
for event in events:
|
||||
print(event[1:9])
|
||||
|
||||
Reference in new issue
Block a user