main
John Lauer 0.2.21 source: script-error auto-recovery + errorShot, symbol_prims/set_models fix for AD 26.9.1, ENGINE_HALTING releas… a577bb1 1mo ago
"""UI Automation helpers — the QUIET way to drive Altium.

Measured 2026-09-01 on AD26.9.1 (test machine), after John asked "you keep turning on the cursor,
but you could maybe use UIA to do those clicks":

Altium exposes a real UIA tree, and far more of it than this bridge was using:

* **Dialogs** (Error / Warning / Confirm / "Connect to Workspace") are plain VCL forms — their
  buttons are reachable by accessible NAME (`OK`, `Choose a license`, `Continue`).
* **The main menus and title bar** are MenuItems: `File`, `View`, `Project`, `Window`, `Help`,
  `Preferences...`, `Share`, `Buy Now`, `Open Home page`, and the workspace item (`Adom`).
* **Every open document is a button** in the document bar — automationId
  `GUIBinderView.TGUIDocBarButton`, accessible name = the tab caption
  ("Bluetooth_Sentinel.PcbDoc", "(2) License Management"). Invoking one makes that document
  CURRENT — no cursor, no foreground. This is the quiet way to satisfy the board verbs'
  "needs a .PcbDoc current" rule.
* **The WebView2 pages** (License Management) expose their DOM through the a11y tree: the
  "Use License" button is `automationId=UseLicenseButton`, `invokable=true`. The bridge used to
  click it at hardcoded pixel coordinates measured on a maximised window.

Why it matters (John's rule): `desktop_click` is SendInput — it needs the target FOREGROUNDED and
it moves the user's pointer. UIA Invoke is programmatic: no foreground, no focus steal, no cursor
movement. So: **UIA first, image-space click only for what UIA genuinely cannot reach.**

Two costs to know:
* A UIA search over Altium's tree is SLOW (seconds, occasionally >60 s under load). Scope every
  call with `hwnd`, keep timeouts generous, and never loop over speculative names.
* Synthetic input (and UIA) is refused outright while a higher-integrity window owns the
  foreground — a screensaver counts. The error names UIPI and says retrying will not help.
"""
from __future__ import annotations

import caller_identity

# Known automationIds, so the common paths skip a name search entirely.
IDS = {
    "use_license": "UseLicenseButton",
}

DOC_TAB_ID = "GUIBinderView.TGUIDocBarButton"


def click(hwnd: int, name: str | None = None, automation_id: str | None = None,
          role: str | None = None, timeout: int = 30) -> dict:
    """Invoke a control programmatically. Returns ab's reply plus `quiet: True` when it worked
    (quiet = the user's cursor never moved and their foreground window was not stolen)."""
    args: dict = {"hwnd": hwnd}
    if automation_id:
        args["automationId"] = automation_id
    if name:
        args["name"] = name
    if role:
        args["role"] = role
    r = caller_identity.ad_callback("desktop_ui_click", args, timeout=timeout) or {}
    if r.get("success"):
        r["quiet"] = True
    return r


def find(hwnd: int, contains: str, role: str | None = None, timeout: int = 60) -> dict:
    args: dict = {"hwnd": hwnd, "contains": contains}
    if role:
        args["role"] = role
    return caller_identity.ad_callback("desktop_find_control", args, timeout=timeout) or {}


def click_by_name_then_pixels(hwnd: int, name: str, pixel_click, automation_id: str | None = None,
                              role: str = "button") -> dict:
    """UIA first (quiet), image-space click only if UIA cannot reach the control.

    `pixel_click` is a zero-arg callable performing the legacy click; it is invoked ONLY on a UIA
    miss, and the result is marked `windowEtiquette: "foreground"` so callers can tell the user
    their screen was touched.
    """
    r = click(hwnd, name=name, automation_id=automation_id or IDS.get(name.lower().replace(" ", "_")), role=role)
    if r.get("success"):
        r["windowEtiquette"] = "background"
        r["lane"] = "uia"
        return r
    out = pixel_click() or {}
    if isinstance(out, dict):
        out["lane"] = "image_click"
        out["windowEtiquette"] = "foreground"
        out["_uiaError"] = r.get("error") or r.get("_hint")
    return out


def activate_document(hwnd: int, caption: str) -> dict:
    """Make an OPEN document current by invoking its tab in the document bar — no cursor, no
    foreground. `caption` is matched against the tab's accessible name; the exact caption carries
    Altium's "(2) " prefixes, so a substring search runs first."""
    hit = find(hwnd, caption)
    best = hit.get("best") or {}
    exact = best.get("name")
    r = click(hwnd, name=exact or caption, role="button")
    if r.get("success"):
        return {"success": True, "activated": exact or caption, "lane": "uia",
                "windowEtiquette": "background",
                "_hint": "The document bar tab was invoked programmatically — the user's cursor never moved."}
    return {"success": False, "error": r.get("error") or "no document tab matched",
            "candidate": exact, "_hint": f"altium_window_info lists open documents; the tab caption may carry a '(n) ' prefix."}