#include "VisualProbeCommon.h" #include #include #include #include #include #include #include #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 activeAnchor{ 1 }; std::atomic compositionActive{ false }; std::atomic closeRequested{ false }; std::atomic nextSequence{ 0 }; std::mutex eventsMutex; std::vector events; }; constexpr std::array 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(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(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 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(anchorIndex)].id; event.threadId = GetCurrentThreadId(); const HKL hkl = GetKeyboardLayout(0); event.hkl = reinterpret_cast(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(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(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(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(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(SetWindowLongPtrA( hwnd, GWLP_WNDPROC, reinterpret_cast(&ProbeWndProc))); const DWORD subclassError = state.previousWndProc ? ERROR_SUCCESS : GetLastError(); std::vector evidence; bool evidenceComplete = state.previousWndProc != nullptr; for (std::size_t index = 0; index < Anchors.size(); ++index) { drawAnchorScreen(static_cast(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(state.previousWndProc)); activeState = nullptr; if (IsWindow(hwnd)) closegraph(); std::vector events; { std::lock_guard lock(state.eventsMutex); events = state.events; } std::array 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; } }