app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
← Commit history
handlers/foreground_guard.pydeleted−138@@ -1,138 +0,0 @@-"""Foreground etiquette for actions that raise a KiCad window.--Two helpers, `warn_before_foreground` and `guarded_foreground`, that every verb-which is KNOWN to raise a window should use. Added 2026-09-01 on John's-instruction, after he watched this bridge repeatedly steal his foreground window.--WHY THIS EXISTS AT ALL-KiCad is wxWidgets, and wx ACTIVATES ITS OWN TOP-LEVEL WINDOW whenever its UI-state changes. Measured on AdomLapper against KiCad 10.0 by comparing z-order-before and after each call (see skills/kicad-uia):-- desktop_ui_tree (read-only) z unchanged- desktop_ui_expand (uia-pattern) KiCad z 4 -> 0- desktop_ui_select (uia-pattern) editor z 1 -> 0- desktop_ui_click (Invoke) editor z 1 -> 0--Note the middle two took the pure UIA pattern path — Adom Bridge never touched-SendInput — and the window STILL came forward. So this is not something a-different ab verb or code path can avoid: you cannot change a wx control's state-without the app raising itself. UI Automation is a way to READ KiCad precisely-and to act without coordinates. It is NOT a way to act invisibly, and it must-never be described to a user as if it were.--THE 200ms RULE (the important part)-When we cause a raise, we hand the user's window straight back, so the raise is a-blip instead of a theft. But the guard gets EXACTLY ONE LOOK, within ~200ms, and-then stops forever for that action.--That bound is the safety property, not a performance tweak. A KiCad window-arriving 50ms after our call is ours. A KiCad window arriving two seconds later-is THE USER — they clicked its taskbar button or alt-tabbed to it. A guard that-keeps watching would fight them: they foreground KiCad, we shove it back, and the-machine feels broken. John's words, 2026-09-01: "after that you need to be hands-off in case the adom user wants to click the windows taskbar icon to foreground-that window or alt-tab to it."--So restoring requires BOTH:- 1. we are still inside the window (default 200ms), AND- 2. the window now on top is the one WE raised — not merely "not the user's".-Fail either and do nothing at all.--CAPTION FIRST-Anything known to foreground announces itself on screen first, via-desktop_caption, so a window appearing is explained rather than surprising. ab-requires a `reason` (>=10 chars) which it prints on the caption beside the thread-name, so the user can always see who is drawing on their screen. Keep the text to-one clause: ab measures the rendered box and warns `too_wide_for_screen`.-"""--from __future__ import annotations--import time--GUARD_WINDOW_MS = 200---def _top_window(ab_call) -> int | None:- """hwnd currently at the top of the z-order, or None if unreadable.- Goes through handlers.kicad_windows, which knows AD's response envelope- (output -> data -> windows); reading res["windows"] directly sees nothing."""- try:- from handlers import kicad_windows- rows = [r for r in (kicad_windows.find(all_windows=True, fresh=True) or []) if r.get("z") is not None]- if not rows:- return None- return sorted(rows, key=lambda r: r.get("z"))[0].get("hwnd")- except Exception: # pylint: disable=broad-except- return None---def warn_before_foreground(ab_call, text: str, reason: str, seconds: int = 5) -> None:- """Tell the user on screen WHY a window is about to jump to the front.-- Best-effort: a caption failure must never block the real work, and must never- turn into an error the user sees twice."""- try:- ab_call("desktop_caption", {"text": text[:120], "seconds": seconds,- "reason": reason})- except Exception: # pylint: disable=broad-except- pass---def guarded_foreground(ab_call, action, *, caption: str | None = None,- reason: str = "", guard_ms: int = GUARD_WINDOW_MS) -> dict:- """Run `action` (which is expected to raise a KiCad window) and give the- user's window back — once, immediately, and only if we are the cause.-- Returns the action's result with a `_foreground` report attached, so a caller- can SAY what happened instead of guessing. Never raises on guard failure: the- user's window placement is a courtesy, not a correctness requirement."""- if caption:- warn_before_foreground(ab_call, caption,- reason or "Warn before an action that raises a KiCad window")-- user_win = _top_window(ab_call)- t0 = time.monotonic()- result = action()- elapsed_ms = (time.monotonic() - t0) * 1000.0-- report = {"userWindowBefore": user_win, "elapsedMs": round(elapsed_ms),- "restored": False, "why": ""}-- # Condition 1: are we still inside the guard window? If the action itself- # took longer than the guard, we have ALREADY missed our chance — anything- # on top now is as likely to be the user as us, so we do not touch it.- if elapsed_ms > guard_ms:- report["why"] = (f"action took {report['elapsedMs']}ms (> {guard_ms}ms guard); "- "too late to tell our raise from a deliberate user switch")- result["_foreground"] = report- return result-- now_top = _top_window(ab_call)- report["topAfter"] = now_top-- # Condition 2: is the window on top the one WE raised? "Not the user's- # window" is NOT good enough — a third app may have come forward on its own,- # and shoving it aside would be the same rudeness in a different direction.- if user_win is None or now_top is None:- report["why"] = "could not read z-order; leaving the user's screen alone"- elif now_top == user_win:- report["why"] = "nothing was raised"- else:- try:- ab_call("desktop_raise_window", {- "hwnd": user_win,- "reason": "Restore the window that was in front before this thread raised a KiCad window"})- report["restored"] = True- report["why"] = "restored the user's window within the guard window"- except Exception as e: # pylint: disable=broad-except- report["why"] = f"restore failed: {type(e).__name__}"-- # No second look. Ever. From here the foreground belongs to the user: if they- # alt-tab to KiCad a moment from now, that is their choice and we leave it.- result["_foreground"] = report- return result