Files
StellarX-kaifa/tests/visual-probe/ImePositionProbe.cpp
T

437 lines
15 KiB
C++

#include "VisualProbeCommon.h"
#include <imm.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <iostream>
#include <mutex>
#include <sstream>
#pragma comment(lib, "Imm32.lib")
namespace VisualProbe
{
namespace
{
struct Anchor
{
const char* id;
POINT controlOrigin;
POINT localCaretTopLeft;
POINT localCaretBottom;
RECT localLineRect;
};
struct ImePositionEvent
{
std::uint64_t sequence = 0;
std::string stage;
std::string positionTrigger = "none";
std::string anchorId;
DWORD threadId = 0;
std::uintptr_t hkl = 0;
std::wstring imeDescription;
std::wstring imeFileName;
bool apiAttempted = false;
bool compositionActiveAtAttempt = false;
bool contextAvailable = false;
bool compositionSucceeded = false;
bool candidateSucceeded = false;
POINT hwndRelativePoint{};
RECT hwndRelativeLineRect{};
POINT screenPoint{};
};
struct ImePositionState
{
HWND hwnd = nullptr;
WNDPROC previousWndProc = nullptr;
std::atomic<int> activeAnchor{ 1 };
std::atomic<bool> compositionActive{ false };
std::atomic<bool> closeRequested{ false };
std::atomic<std::uint64_t> nextSequence{ 0 };
std::mutex eventsMutex;
std::vector<ImePositionEvent> events;
};
constexpr std::array<Anchor, 3> Anchors{
Anchor{ "anchor_top_left_safe", POINT{ 80, 125 }, POINT{ 30, 35 }, POINT{ 30, 61 }, RECT{ 10, 30, 250, 66 } },
Anchor{ "anchor_center", POINT{ 350, 305 }, POINT{ 130, 55 }, POINT{ 130, 81 }, RECT{ 10, 50, 260, 86 } },
Anchor{ "anchor_bottom_right_safe", POINT{ 650, 520 }, POINT{ 180, 45 }, POINT{ 180, 71 }, RECT{ 10, 40, 230, 76 } }
};
ImePositionState* activeState = nullptr;
POINT surfaceCaret(const Anchor& anchor) noexcept
{
return POINT{
anchor.controlOrigin.x + anchor.localCaretBottom.x,
anchor.controlOrigin.y + anchor.localCaretBottom.y
};
}
std::wstring imeDescription(HKL hkl)
{
const UINT length = ImmGetDescriptionW(hkl, nullptr, 0);
if (!length) return {};
std::wstring value(length + 1, L'\0');
const UINT written = ImmGetDescriptionW(hkl, value.data(), static_cast<UINT>(value.size()));
value.resize(written);
return value;
}
std::wstring imeFileName(HKL hkl)
{
const UINT length = ImmGetIMEFileNameW(hkl, nullptr, 0);
if (!length) return {};
std::wstring value(length + 1, L'\0');
const UINT written = ImmGetIMEFileNameW(hkl, value.data(), static_cast<UINT>(value.size()));
value.resize(written);
return value;
}
void appendEvent(ImePositionState& state, ImePositionEvent event)
{
event.sequence = state.nextSequence.fetch_add(1, std::memory_order_relaxed) + 1;
std::lock_guard<std::mutex> lock(state.eventsMutex);
state.events.push_back(std::move(event));
}
void recordLifecycle(ImePositionState& state, const char* stage)
{
const int anchorIndex = state.activeAnchor.load(std::memory_order_acquire);
ImePositionEvent event;
event.stage = stage;
event.anchorId = Anchors[static_cast<std::size_t>(anchorIndex)].id;
event.threadId = GetCurrentThreadId();
const HKL hkl = GetKeyboardLayout(0);
event.hkl = reinterpret_cast<std::uintptr_t>(hkl);
event.imeDescription = imeDescription(hkl);
event.imeFileName = imeFileName(hkl);
appendEvent(state, std::move(event));
}
void configureImePosition(ImePositionState& state, const char* stage, const char* positionTrigger)
{
const int anchorIndex = state.activeAnchor.load(std::memory_order_acquire);
const Anchor& anchor = Anchors[static_cast<std::size_t>(anchorIndex)];
ImePositionEvent event;
event.stage = stage;
event.positionTrigger = positionTrigger;
event.anchorId = anchor.id;
event.threadId = GetCurrentThreadId();
const HKL hkl = GetKeyboardLayout(0);
event.hkl = reinterpret_cast<std::uintptr_t>(hkl);
event.imeDescription = imeDescription(hkl);
event.imeFileName = imeFileName(hkl);
event.apiAttempted = true;
event.compositionActiveAtAttempt = state.compositionActive.load(std::memory_order_acquire);
event.hwndRelativePoint = surfaceCaret(anchor);
event.hwndRelativeLineRect = RECT{
anchor.controlOrigin.x + anchor.localLineRect.left,
anchor.controlOrigin.y + anchor.localLineRect.top,
anchor.controlOrigin.x + anchor.localLineRect.right,
anchor.controlOrigin.y + anchor.localLineRect.bottom
};
event.screenPoint = event.hwndRelativePoint;
ClientToScreen(state.hwnd, &event.screenPoint);
HIMC context = ImmGetContext(state.hwnd);
event.contextAvailable = context != 0;
if (context)
{
COMPOSITIONFORM composition{};
composition.dwStyle = CFS_POINT;
composition.ptCurrentPos = event.hwndRelativePoint;
event.compositionSucceeded = ImmSetCompositionWindow(context, &composition) != FALSE;
CANDIDATEFORM candidate{};
candidate.dwIndex = 0;
candidate.dwStyle = CFS_EXCLUDE;
candidate.ptCurrentPos = event.hwndRelativePoint;
candidate.rcArea = event.hwndRelativeLineRect;
event.candidateSucceeded = ImmSetCandidateWindow(context, &candidate) != FALSE;
ImmReleaseContext(state.hwnd, context);
}
appendEvent(state, std::move(event));
}
LRESULT CALLBACK ProbeWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
ImePositionState* state = activeState;
if (!state)
return DefWindowProcA(hwnd, message, wParam, lParam);
if (message == WM_KEYDOWN && wParam >= VK_F1 && wParam <= VK_F3)
{
state->activeAnchor.store(static_cast<int>(wParam - VK_F1), std::memory_order_release);
configureImePosition(*state, "anchor_preconfigure", "anchor_change");
return 0;
}
else if (message == WM_IME_STARTCOMPOSITION)
{
state->compositionActive.store(true, std::memory_order_release);
configureImePosition(*state, "ime_start", "ime_lifecycle");
}
else if (message == WM_IME_COMPOSITION)
{
configureImePosition(*state, "ime_composition", "ime_lifecycle");
}
else if (message == WM_IME_ENDCOMPOSITION)
{
recordLifecycle(*state, "ime_end");
state->compositionActive.store(false, std::memory_order_release);
}
else if (message == WM_INPUTLANGCHANGE)
{
recordLifecycle(*state, "input_language_change");
}
else if (message == WM_CLOSE)
{
recordLifecycle(*state, "close_requested");
state->closeRequested.store(true, std::memory_order_release);
return 0;
}
return state->previousWndProc
? CallWindowProcA(state->previousWndProc, hwnd, message, wParam, lParam)
: DefWindowProcA(hwnd, message, wParam, lParam);
}
void drawAnchorScreen(int activeAnchorIndex)
{
SetWorkingImage(nullptr);
BeginBatchDraw();
setbkcolor(RGB(238, 241, 246));
cleardevice();
setbkmode(TRANSPARENT);
settextstyle(22, 0, "Microsoft YaHei UI");
settextcolor(RGB(30, 45, 70));
outtextxy(30, 25, "IME position probe: F1/F2/F3 select anchor; close window when done");
outtextxy(30, 58, "Use only fixed non-sensitive input: ceshi; inspect candidate position; Esc cancels");
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
const Anchor& anchor = Anchors[index];
const bool selected = static_cast<int>(index) == activeAnchorIndex;
const COLORREF color = selected ? RGB(220, 60, 45) : RGB(45, 125, 190);
const POINT caret = surfaceCaret(anchor);
setlinecolor(color);
setlinestyle(PS_SOLID, selected ? 3 : 1);
rectangle(anchor.controlOrigin.x, anchor.controlOrigin.y,
anchor.controlOrigin.x + anchor.localLineRect.right,
anchor.controlOrigin.y + anchor.localLineRect.bottom + 12);
line(caret.x, caret.y - 26, caret.x, caret.y + 4);
line(caret.x - 5, caret.y, caret.x + 5, caret.y);
settextcolor(color);
const std::string label = std::string("F") + std::to_string(index + 1) + " " + anchor.id;
outtextxy(anchor.controlOrigin.x + 8, anchor.controlOrigin.y + 5, label.c_str());
}
EndBatchDraw();
}
std::string normalizedRelative(const std::filesystem::path& path)
{
std::string value = path.generic_string();
std::replace(value.begin(), value.end(), '\\', '/');
return value;
}
void writeAnchor(std::ostream& output, const Anchor& anchor)
{
const POINT surfaceTopLeft{
anchor.controlOrigin.x + anchor.localCaretTopLeft.x,
anchor.controlOrigin.y + anchor.localCaretTopLeft.y
};
const POINT surfaceBottom = surfaceCaret(anchor);
RECT surfaceLine{
anchor.controlOrigin.x + anchor.localLineRect.left,
anchor.controlOrigin.y + anchor.localLineRect.top,
anchor.controlOrigin.x + anchor.localLineRect.right,
anchor.controlOrigin.y + anchor.localLineRect.bottom
};
output << "{\"syntheticAnchorId\":" << jsonString(anchor.id)
<< ",\"controlOrigin\":";
writePointJson(output, anchor.controlOrigin);
output << ",\"controlLocalCaretTopLeft\":";
writePointJson(output, anchor.localCaretTopLeft);
output << ",\"controlLocalCaretBottom\":";
writePointJson(output, anchor.localCaretBottom);
output << ",\"controlLocalLineRect\":";
writeRectJson(output, anchor.localLineRect);
output << ",\"easyxSurfaceCaretTopLeft\":";
writePointJson(output, surfaceTopLeft);
output << ",\"easyxSurfaceCaretBottom\":";
writePointJson(output, surfaceBottom);
output << ",\"easyxSurfaceLineRect\":";
writeRectJson(output, surfaceLine);
output << ",\"targetHwndRelativePoint\":";
writePointJson(output, surfaceBottom);
output << '}';
}
void writeEvent(std::ostream& output, const ImePositionEvent& event)
{
output << "{\"sequence\":" << event.sequence
<< ",\"stage\":" << jsonString(event.stage)
<< ",\"positionTrigger\":" << jsonString(event.positionTrigger)
<< ",\"syntheticAnchorId\":" << jsonString(event.anchorId)
<< ",\"threadId\":" << event.threadId
<< ",\"hkl\":" << jsonString(hexValue(event.hkl))
<< ",\"imeDescriptionUtf16\":" << utf16UnitsJson(event.imeDescription)
<< ",\"imeFileNameUtf16\":" << utf16UnitsJson(event.imeFileName)
<< ",\"apiAttempted\":";
writeBoolean(output, event.apiAttempted);
output << ",\"compositionActiveAtAttempt\":";
writeBoolean(output, event.compositionActiveAtAttempt);
output << ",\"contextAvailable\":";
writeBoolean(output, event.contextAvailable);
output << ",\"compositionForm\":{\"style\":" << CFS_POINT
<< ",\"apiResult\":";
writeBoolean(output, event.compositionSucceeded);
output << "},\"candidateForm\":{\"style\":" << CFS_EXCLUDE
<< ",\"index\":0,\"apiResult\":";
writeBoolean(output, event.candidateSucceeded);
output << ",\"excludeRect\":";
writeRectJson(output, event.hwndRelativeLineRect);
output << "},\"targetHwndRelativePoint\":";
writePointJson(output, event.hwndRelativePoint);
output << ",\"diagnosticScreenPoint\":";
writePointJson(output, event.screenPoint);
output << '}';
}
}
int runImePositionProbe(const Options& options, const DpiConfiguration& dpiConfiguration)
{
HWND hwnd = initgraph(CanvasWidth, CanvasHeight, EX_SHOWCONSOLE);
if (!hwnd)
{
std::cerr << "initgraph failed\n";
return 3;
}
SetWindowTextA(hwnd, "StellarX 0C IME Position Probe");
ShowWindow(hwnd, SW_SHOWNORMAL);
SetForegroundWindow(hwnd);
Sleep(80);
const EnvironmentSnapshot environment = captureEnvironment(hwnd);
DWORD windowProcessId = 0;
const DWORD windowOwnerThread = GetWindowThreadProcessId(hwnd, &windowProcessId);
ImePositionState state;
state.hwnd = hwnd;
activeState = &state;
SetLastError(ERROR_SUCCESS);
state.previousWndProc = reinterpret_cast<WNDPROC>(SetWindowLongPtrA(
hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&ProbeWndProc)));
const DWORD subclassError = state.previousWndProc ? ERROR_SUCCESS : GetLastError();
std::vector<EvidenceFile> evidence;
bool evidenceComplete = state.previousWndProc != nullptr;
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
drawAnchorScreen(static_cast<int>(index));
Sleep(40);
const auto relative = std::filesystem::path("evidence") /
(std::string(Anchors[index].id) + "_client.png");
std::string backend;
const bool saved = captureClientImage(hwnd, options.outputDirectory / relative, &backend);
evidenceComplete = evidenceComplete && saved;
if (saved)
evidence.push_back({ normalizedRelative(relative), "synthetic anchor client evidence", backend });
}
state.activeAnchor.store(1, std::memory_order_release);
drawAnchorScreen(1);
int lastDrawnAnchor = 1;
const ULONGLONG loopStarted = GetTickCount64();
while (!state.closeRequested.load(std::memory_order_acquire) && IsWindow(hwnd))
{
if (options.autoCloseMilliseconds > 0 &&
GetTickCount64() - loopStarted >= options.autoCloseMilliseconds)
{
recordLifecycle(state, "auto_close");
state.closeRequested.store(true, std::memory_order_release);
break;
}
const int currentAnchor = state.activeAnchor.load(std::memory_order_acquire);
if (currentAnchor != lastDrawnAnchor)
{
drawAnchorScreen(currentAnchor);
lastDrawnAnchor = currentAnchor;
}
Sleep(16);
}
if (state.previousWndProc && IsWindow(hwnd))
SetWindowLongPtrA(hwnd, GWLP_WNDPROC,
reinterpret_cast<LONG_PTR>(state.previousWndProc));
activeState = nullptr;
if (IsWindow(hwnd)) closegraph();
std::vector<ImePositionEvent> events;
{
std::lock_guard<std::mutex> lock(state.eventsMutex);
events = state.events;
}
std::array<bool, Anchors.size()> anchorSucceeded{};
bool imeLifecycleObserved = false;
bool imeLifecycleSucceeded = false;
for (const auto& event : events)
{
if (event.positionTrigger == "ime_lifecycle") imeLifecycleObserved = true;
if (event.positionTrigger == "ime_lifecycle" && event.contextAvailable &&
event.compositionSucceeded && event.candidateSucceeded)
{
imeLifecycleSucceeded = true;
}
if (!event.apiAttempted || !event.compositionSucceeded || !event.candidateSucceeded)
continue;
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
if (event.anchorId == Anchors[index].id) anchorSucceeded[index] = true;
}
}
bool allAnchorsSucceeded = true;
for (const bool value : anchorSucceeded) allAnchorsSucceeded = allAnchorsSucceeded && value;
const bool completed = evidenceComplete && allAnchorsSucceeded && imeLifecycleSucceeded;
std::ostringstream result;
result << "{\"expectedImeName\":" << jsonString(options.expectedImeName)
<< ",\"windowOwnerThreadId\":" << windowOwnerThread
<< ",\"windowProcessId\":" << windowProcessId
<< ",\"subclassInstalled\":";
writeBoolean(result, state.previousWndProc != nullptr);
result << ",\"subclassError\":" << subclassError
<< ",\"allAnchorsApiSucceeded\":";
writeBoolean(result, allAnchorsSucceeded);
result << ",\"imeLifecycleObserved\":";
writeBoolean(result, imeLifecycleObserved);
result << ",\"imeLifecycleSucceeded\":";
writeBoolean(result, imeLifecycleSucceeded);
result << ",\"anchors\":[";
for (std::size_t index = 0; index < Anchors.size(); ++index)
{
if (index) result << ',';
writeAnchor(result, Anchors[index]);
}
result << "],\"imeEvents\":[";
for (std::size_t index = 0; index < events.size(); ++index)
{
if (index) result << ',';
writeEvent(result, events[index]);
}
result << "]}";
std::string error;
if (!writeRawResult(options, dpiConfiguration, environment, completed,
evidence, result.str(), error))
{
std::cerr << error << '\n';
return 4;
}
return completed ? 0 : 5;
}
}