← Commit history

Delete handlers/foreground_guard.py

John Lauer ·d0e91403bc ·29d ago ·parent 99b8f18
1 file changed −138
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