169 lines
6.7 KiB
C++
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();
|
|
}
|