Files
wine-access/msaa_events.cpp
T

169 lines
6.7 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include "msaa_events.h"
#include "msaa_states.h"
#include "text_limits.h"
#include <oleacc.h>
#include <condition_variable>
#include <mutex>
#include <deque>
#include <atomic>
#include <utility>
namespace {
// MSAA notifications describe transient state, unlike ordered editor actions.
// Bound pending work and favor current focus if an application floods events.
constexpr size_t capacity = 32;
std::string text(BSTR value) {
return bounded_accessible_text(value, max_accessible_units);
}
std::string property(IAccessible *object,
HRESULT (STDMETHODCALLTYPE IAccessible::*getter)(VARIANT, BSTR *), VARIANT child) {
BSTR value = nullptr;
HRESULT hr = (object->*getter)(child, &value);
auto result = SUCCEEDED(hr) ? bounded_accessible_text(value, value ? SysStringLen(value) : 0) : std::string{};
SysFreeString(value);
return result;
}
struct Accessible {
IAccessible *object = nullptr;
VARIANT child{};
~Accessible() { if (object) object->Release(); VariantClear(&child); }
};
}
bool msaa_event_in_foreground(HWND window) {
HWND foreground = GetForegroundWindow();
HWND root = GetAncestor(window, GA_ROOT);
if (!foreground || !root) return false;
if (root == foreground) return true;
if (!IsWindowVisible(root) || !(GetWindowLongPtrW(root, GWL_STYLE) & WS_POPUP)) return false;
// Bound traversal even if a malformed owner chain cycles. Owned popups can
// expose MSAA focus while the foreground owner retains native keyboard focus.
for (unsigned depth = 0; root && depth < 32; ++depth) {
root = GetWindow(root, GW_OWNER);
if (root == foreground) return true;
}
return false;
}
struct MsaaEventReader::State {
std::mutex mutex;
std::condition_variable changed;
std::deque<MsaaEvent> pending;
std::deque<MsaaResult> ready;
std::atomic<bool> stopping{false};
uint64_t generation = 0;
bool current(const MsaaEvent &event) {
if (stopping || GetTickCount64() >= event.deadline ||
!msaa_event_in_foreground(event.window)) return false;
std::lock_guard<std::mutex> lock(mutex);
return generation == event.generation;
}
MsaaResult read(const MsaaEvent &event) {
MsaaResult result; result.source = event;
Accessible accessible;
if (!current(event) || FAILED(AccessibleObjectFromEvent(event.window, event.object,
event.child, &accessible.object, &accessible.child)) || !accessible.object)
return result;
if (!current(event)) return result;
VARIANT role{};
LONG roleId = 0;
if (SUCCEEDED(accessible.object->get_accRole(accessible.child, &role))) {
if (role.vt == VT_I4) {
roleId = role.lVal;
WCHAR label[128]{};
if (GetRoleTextW(roleId, label, 128)) result.role = text(label);
else result.role = std::to_string(roleId);
} else if (role.vt == VT_BSTR)
result.role = bounded_accessible_text(role.bstrVal, role.bstrVal ? SysStringLen(role.bstrVal) : 0);
}
VariantClear(&role);
if (!current(event)) return result;
VARIANT state{};
if (SUCCEEDED(accessible.object->get_accState(accessible.child, &state)) && state.vt == VT_I4) {
result.states = msaa_states(static_cast<DWORD>(state.lVal), roleId);
result.password = state.lVal & STATE_SYSTEM_PROTECTED;
}
VariantClear(&state);
if (!current(event)) return result;
result.name = property(accessible.object, &IAccessible::get_accName, accessible.child);
if (!current(event)) return result;
// Protected values must be excluded before querying the provider.
if (!result.password) result.value = property(accessible.object, &IAccessible::get_accValue, accessible.child);
if (!current(event)) return result;
result.description = property(accessible.object, &IAccessible::get_accDescription, accessible.child);
if (!current(event)) return result;
accessible.object->get_accChildCount(&result.count);
result.native_client = accessible.child.vt == VT_I4 && accessible.child.lVal == CHILDID_SELF &&
(result.role.empty() || roleId == ROLE_SYSTEM_CLIENT);
result.available = true;
return 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_APARTMENTTHREADED);
for (;;) {
MsaaEvent event;
{
std::unique_lock<std::mutex> lock(state->mutex);
state->changed.wait(lock, [&] { return state->stopping || !state->pending.empty(); });
if (state->stopping) break;
event = state->pending.front(); state->pending.pop_front();
}
auto result = SUCCEEDED(com) ? state->read(event) : MsaaResult{};
result.source = event;
if (!state->current(event)) continue;
std::lock_guard<std::mutex> lock(state->mutex);
if (state->stopping || event.generation != state->generation) continue;
if (state->ready.size() == capacity) state->ready.pop_front();
state->ready.push_back(std::move(result));
}
if (SUCCEEDED(com)) CoUninitialize();
return 0;
}
};
MsaaEventReader::~MsaaEventReader() { stop(); }
bool MsaaEventReader::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 MsaaEventReader::invalidate(uint64_t generation) {
if (!state_) return;
std::lock_guard<std::mutex> lock(state_->mutex);
state_->generation = generation;
state_->pending.clear(); state_->ready.clear();
}
void MsaaEventReader::submit(const MsaaEvent &event) {
if (!state_) return;
std::lock_guard<std::mutex> lock(state_->mutex);
if (state_->stopping || state_->generation != event.generation) return;
if (state_->pending.size() == capacity) state_->pending.pop_front();
state_->pending.push_back(event); state_->changed.notify_one();
}
bool MsaaEventReader::take(MsaaResult &result) {
if (!state_) return false;
std::lock_guard<std::mutex> lock(state_->mutex);
if (state_->ready.empty()) return false;
result = std::move(state_->ready.front()); state_->ready.pop_front();
return true;
}
void MsaaEventReader::stop() {
if (!state_) return;
{
std::lock_guard<std::mutex> lock(state_->mutex);
state_->stopping = true; state_->pending.clear(); state_->ready.clear();
state_->changed.notify_one();
}
// A hung COM call cannot be safely terminated. It retains only its own
// shared state and native references until completion or process exit.
WaitForSingleObject(worker_, 100);
CloseHandle(worker_); worker_ = nullptr;
state_.reset();
}