|
|
|
@@ -0,0 +1,282 @@
|
|
|
|
|
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
|
|
|
|
|
|
#include "nvdaController.h"
|
|
|
|
|
|
|
|
|
|
#include <gio/gio.h>
|
|
|
|
|
#include <oleacc.h>
|
|
|
|
|
#include <rpc.h>
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <cwchar>
|
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
constexpr auto BUS_NAME = "org.stormux.Cthulhu1.Service";
|
|
|
|
|
constexpr auto OBJECT_PATH = "/org/stormux/Cthulhu1/Service";
|
|
|
|
|
constexpr auto INTERFACE_NAME = "org.stormux.Cthulhu1.Service";
|
|
|
|
|
constexpr int DBUS_TIMEOUT_MS = 500;
|
|
|
|
|
GDBusConnection *connection = nullptr;
|
|
|
|
|
|
|
|
|
|
std::string to_utf8(const wchar_t *text) {
|
|
|
|
|
if (text == nullptr)
|
|
|
|
|
return {};
|
|
|
|
|
GError *error = nullptr;
|
|
|
|
|
gchar *converted = g_utf16_to_utf8(reinterpret_cast<const gunichar2 *>(text),
|
|
|
|
|
-1, nullptr, nullptr, &error);
|
|
|
|
|
if (error != nullptr)
|
|
|
|
|
g_error_free(error);
|
|
|
|
|
if (converted == nullptr)
|
|
|
|
|
return {};
|
|
|
|
|
std::string result{converted};
|
|
|
|
|
g_free(converted);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DWORD call_boolean(const char *method, GVariant *parameters) {
|
|
|
|
|
if (connection == nullptr)
|
|
|
|
|
return ERROR_SERVICE_NOT_ACTIVE;
|
|
|
|
|
GError *error = nullptr;
|
|
|
|
|
GVariant *reply = g_dbus_connection_call_sync(
|
|
|
|
|
connection, BUS_NAME, OBJECT_PATH, INTERFACE_NAME, method, parameters,
|
|
|
|
|
G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
|
|
|
|
&error);
|
|
|
|
|
if (error != nullptr)
|
|
|
|
|
g_error_free(error);
|
|
|
|
|
if (reply == nullptr)
|
|
|
|
|
return ERROR_GEN_FAILURE;
|
|
|
|
|
gboolean result = FALSE;
|
|
|
|
|
g_variant_get(reply, "(b)", &result);
|
|
|
|
|
g_variant_unref(reply);
|
|
|
|
|
return result == TRUE ? ERROR_SUCCESS : ERROR_GEN_FAILURE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool cthulhu_is_available() {
|
|
|
|
|
GError *error = nullptr;
|
|
|
|
|
GVariant *reply = g_dbus_connection_call_sync(
|
|
|
|
|
connection, "org.freedesktop.DBus", "/org/freedesktop/DBus",
|
|
|
|
|
"org.freedesktop.DBus", "NameHasOwner", g_variant_new("(s)", BUS_NAME),
|
|
|
|
|
G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
|
|
|
|
&error);
|
|
|
|
|
if (error != nullptr)
|
|
|
|
|
g_error_free(error);
|
|
|
|
|
if (reply == nullptr)
|
|
|
|
|
return false;
|
|
|
|
|
gboolean owned = FALSE;
|
|
|
|
|
g_variant_get(reply, "(b)", &owned);
|
|
|
|
|
g_variant_unref(reply);
|
|
|
|
|
return owned == TRUE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string variant_text(const VARIANT &value) {
|
|
|
|
|
if (value.vt == VT_BSTR)
|
|
|
|
|
return to_utf8(value.bstrVal);
|
|
|
|
|
if (value.vt == VT_I4) {
|
|
|
|
|
wchar_t buffer[128] = {};
|
|
|
|
|
if (GetRoleTextW(value.lVal, buffer, 128) > 0)
|
|
|
|
|
return to_utf8(buffer);
|
|
|
|
|
return std::to_string(value.lVal);
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string accessible_property(
|
|
|
|
|
IAccessible *accessible,
|
|
|
|
|
HRESULT (STDMETHODCALLTYPE IAccessible::*getter)(VARIANT, BSTR *),
|
|
|
|
|
const VARIANT &child) {
|
|
|
|
|
BSTR value = nullptr;
|
|
|
|
|
const HRESULT result = (accessible->*getter)(child, &value);
|
|
|
|
|
if (FAILED(result) || value == nullptr)
|
|
|
|
|
return {};
|
|
|
|
|
const std::string text = to_utf8(value);
|
|
|
|
|
SysFreeString(value);
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CALLBACK winevent_callback(HWINEVENTHOOK, DWORD event, HWND window,
|
|
|
|
|
LONG objectId, LONG childId, DWORD, DWORD) {
|
|
|
|
|
if (event != EVENT_OBJECT_FOCUS && event != EVENT_OBJECT_NAMECHANGE &&
|
|
|
|
|
event != EVENT_OBJECT_VALUECHANGE && event != EVENT_OBJECT_SELECTION &&
|
|
|
|
|
event != EVENT_OBJECT_STATECHANGE)
|
|
|
|
|
return;
|
|
|
|
|
IAccessible *accessible = nullptr;
|
|
|
|
|
VARIANT child{};
|
|
|
|
|
VariantInit(&child);
|
|
|
|
|
if (FAILED(AccessibleObjectFromEvent(window, objectId, childId, &accessible,
|
|
|
|
|
&child)) ||
|
|
|
|
|
accessible == nullptr)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const std::string name =
|
|
|
|
|
accessible_property(accessible, &IAccessible::get_accName, child);
|
|
|
|
|
const std::string value =
|
|
|
|
|
accessible_property(accessible, &IAccessible::get_accValue, child);
|
|
|
|
|
const std::string description =
|
|
|
|
|
accessible_property(accessible, &IAccessible::get_accDescription, child);
|
|
|
|
|
VARIANT role{};
|
|
|
|
|
VariantInit(&role);
|
|
|
|
|
std::string roleText;
|
|
|
|
|
if (SUCCEEDED(accessible->get_accRole(child, &role)))
|
|
|
|
|
roleText = variant_text(role);
|
|
|
|
|
VariantClear(&role);
|
|
|
|
|
VARIANT state{};
|
|
|
|
|
VariantInit(&state);
|
|
|
|
|
std::string stateText;
|
|
|
|
|
if (SUCCEEDED(accessible->get_accState(child, &state)))
|
|
|
|
|
stateText = variant_text(state);
|
|
|
|
|
VariantClear(&state);
|
|
|
|
|
LONG childCount = 0;
|
|
|
|
|
accessible->get_accChildCount(&childCount);
|
|
|
|
|
accessible->Release();
|
|
|
|
|
VariantClear(&child);
|
|
|
|
|
|
|
|
|
|
wchar_t title[512] = {};
|
|
|
|
|
GetWindowTextW(GetAncestor(window, GA_ROOT), title, 512);
|
|
|
|
|
const std::string windowTitle = to_utf8(title);
|
|
|
|
|
const char *eventType = event == EVENT_OBJECT_FOCUS ? "focus" : "change";
|
|
|
|
|
call_boolean("PresentAccessibleEvent",
|
|
|
|
|
g_variant_new("(sssssssiis)", "msaa", eventType, name.c_str(),
|
|
|
|
|
roleText.c_str(), value.c_str(), stateText.c_str(),
|
|
|
|
|
description.c_str(), 0,
|
|
|
|
|
static_cast<int>(childCount),
|
|
|
|
|
windowTitle.c_str()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool register_rpc_server() {
|
|
|
|
|
if (RpcServerUseProtseqEpW(
|
|
|
|
|
reinterpret_cast<RPC_WSTR>(const_cast<wchar_t *>(L"ncalrpc")),
|
|
|
|
|
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
|
|
|
|
|
reinterpret_cast<RPC_WSTR>(const_cast<wchar_t *>(L"NvdaCtlr")),
|
|
|
|
|
nullptr) != RPC_S_OK)
|
|
|
|
|
return false;
|
|
|
|
|
DWORD sessionId = 0;
|
|
|
|
|
if (!ProcessIdToSessionId(GetCurrentProcessId(), &sessionId))
|
|
|
|
|
return false;
|
|
|
|
|
wchar_t desktopName[256] = {};
|
|
|
|
|
DWORD bytesNeeded = 0;
|
|
|
|
|
if (!GetUserObjectInformationW(GetThreadDesktop(GetCurrentThreadId()),
|
|
|
|
|
UOI_NAME, desktopName, sizeof(desktopName),
|
|
|
|
|
&bytesNeeded))
|
|
|
|
|
return false;
|
|
|
|
|
wchar_t endpoint[512] = {};
|
|
|
|
|
wsprintfW(endpoint, L"NvdaCtlr.%lu.%s", sessionId, desktopName);
|
|
|
|
|
if (RpcServerUseProtseqEpW(
|
|
|
|
|
reinterpret_cast<RPC_WSTR>(const_cast<wchar_t *>(L"ncalrpc")),
|
|
|
|
|
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
|
|
|
|
|
reinterpret_cast<RPC_WSTR>(endpoint),
|
|
|
|
|
nullptr) != RPC_S_OK)
|
|
|
|
|
return false;
|
|
|
|
|
for (RPC_IF_HANDLE spec :
|
|
|
|
|
{NvdaController_v1_0_s_ifspec, NvdaController2_v1_0_s_ifspec,
|
|
|
|
|
NvdaController3_v1_0_s_ifspec}) {
|
|
|
|
|
if (RpcServerRegisterIf(spec, nullptr, nullptr) != RPC_S_OK)
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return RpcServerListen(1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, TRUE) == RPC_S_OK;
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
extern "C" void *__RPC_USER MIDL_user_allocate(size_t size) {
|
|
|
|
|
return malloc(size);
|
|
|
|
|
}
|
|
|
|
|
extern "C" void __RPC_USER MIDL_user_free(void *pointer) { free(pointer); }
|
|
|
|
|
|
|
|
|
|
extern "C" error_status_t __stdcall testIfRunning(void) {
|
|
|
|
|
return cthulhu_is_available() ? ERROR_SUCCESS : ERROR_SERVICE_NOT_ACTIVE;
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall speakText(const wchar_t *text) {
|
|
|
|
|
const std::string utf8 = to_utf8(text);
|
|
|
|
|
return call_boolean("SpeakText", g_variant_new("(sb)", utf8.c_str(), TRUE));
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall cancelSpeech(void) {
|
|
|
|
|
return call_boolean("CancelSpeech", nullptr);
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall brailleMessage(const wchar_t *text) {
|
|
|
|
|
const std::string utf8 = to_utf8(text);
|
|
|
|
|
return call_boolean("BrailleMessage", g_variant_new("(s)", utf8.c_str()));
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall getProcessId(ULONG *pid) {
|
|
|
|
|
if (pid == nullptr)
|
|
|
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
|
*pid = GetCurrentProcessId();
|
|
|
|
|
return ERROR_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall speakSsml(const wchar_t *ssml, SYMBOL_LEVEL,
|
|
|
|
|
SPEECH_PRIORITY, boolean) {
|
|
|
|
|
const std::string utf8 = to_utf8(ssml);
|
|
|
|
|
GError *error = nullptr;
|
|
|
|
|
GVariant *reply = g_dbus_connection_call_sync(
|
|
|
|
|
connection, BUS_NAME, OBJECT_PATH, INTERFACE_NAME, "SpeakSsml",
|
|
|
|
|
g_variant_new("(ss)", "wine-nvda-controller", utf8.c_str()),
|
|
|
|
|
G_VARIANT_TYPE("(i)"), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, nullptr,
|
|
|
|
|
&error);
|
|
|
|
|
if (error != nullptr)
|
|
|
|
|
g_error_free(error);
|
|
|
|
|
if (reply == nullptr)
|
|
|
|
|
return ERROR_GEN_FAILURE;
|
|
|
|
|
gint result = ERROR_GEN_FAILURE;
|
|
|
|
|
g_variant_get(reply, "(i)", &result);
|
|
|
|
|
g_variant_unref(reply);
|
|
|
|
|
return static_cast<error_status_t>(result);
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall onSsmlMarkReached(const wchar_t *) {
|
|
|
|
|
return ERROR_CALL_NOT_IMPLEMENTED;
|
|
|
|
|
}
|
|
|
|
|
extern "C" error_status_t __stdcall isSpeaking(boolean *speaking) {
|
|
|
|
|
if (speaking == nullptr)
|
|
|
|
|
return ERROR_INVALID_PARAMETER;
|
|
|
|
|
*speaking = FALSE;
|
|
|
|
|
return ERROR_CALL_NOT_IMPLEMENTED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int WINAPI WinMain(HINSTANCE instance, HINSTANCE, char *, int) {
|
|
|
|
|
const wchar_t *commandLine = GetCommandLineW();
|
|
|
|
|
const bool dialogReaderEnabled =
|
|
|
|
|
wcsstr(commandLine, L"--no-dialog-reader") == nullptr;
|
|
|
|
|
const bool controllerEnabled =
|
|
|
|
|
wcsstr(commandLine, L"--no-controller") == nullptr;
|
|
|
|
|
if (!dialogReaderEnabled && !controllerEnabled)
|
|
|
|
|
return 0;
|
|
|
|
|
if (controllerEnabled && FindWindowW(L"wxWindowClassNR", L"NVDA") != nullptr)
|
|
|
|
|
return 0;
|
|
|
|
|
GError *error = nullptr;
|
|
|
|
|
connection = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error);
|
|
|
|
|
if (error != nullptr)
|
|
|
|
|
g_error_free(error);
|
|
|
|
|
if (connection == nullptr || !cthulhu_is_available())
|
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
|
|
HWND window = nullptr;
|
|
|
|
|
if (controllerEnabled) {
|
|
|
|
|
WNDCLASSW windowClass{};
|
|
|
|
|
windowClass.lpfnWndProc = DefWindowProcW;
|
|
|
|
|
windowClass.hInstance = instance;
|
|
|
|
|
windowClass.lpszClassName = L"wxWindowClassNR";
|
|
|
|
|
RegisterClassW(&windowClass);
|
|
|
|
|
window = CreateWindowW(windowClass.lpszClassName, L"NVDA", 0, 0, 0, 0, 0,
|
|
|
|
|
nullptr, nullptr, instance, nullptr);
|
|
|
|
|
}
|
|
|
|
|
if ((controllerEnabled && (window == nullptr || !register_rpc_server()))) {
|
|
|
|
|
g_object_unref(connection);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
HWINEVENTHOOK hook = nullptr;
|
|
|
|
|
if (dialogReaderEnabled) {
|
|
|
|
|
hook = SetWinEventHook(EVENT_MIN, EVENT_MAX, nullptr, winevent_callback, 0,
|
|
|
|
|
0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
|
|
|
|
|
}
|
|
|
|
|
MSG message{};
|
|
|
|
|
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
|
|
|
|
TranslateMessage(&message);
|
|
|
|
|
DispatchMessageW(&message);
|
|
|
|
|
}
|
|
|
|
|
if (hook != nullptr)
|
|
|
|
|
UnhookWinEvent(hook);
|
|
|
|
|
if (controllerEnabled) {
|
|
|
|
|
RpcMgmtStopServerListening(nullptr);
|
|
|
|
|
RpcServerUnregisterIf(nullptr, nullptr, FALSE);
|
|
|
|
|
DestroyWindow(window);
|
|
|
|
|
}
|
|
|
|
|
g_object_unref(connection);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|