Files
wine-access/bootstrapper_controls.cpp
T

210 lines
9.2 KiB
C++

// 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();
}