← Commit history

0.9.285: Foreground etiquette on the Symbol Editor launch (wiki #71 field test): before opening or auto-rescan-relaunching the editor the bridge captions why a KiCad window is about to appear, remembers the user's front window, and the moment the new frame exists looks ONCE at the z-order; if the frame landed above a non-KiCad user window it slides the frame back behind it with SetWindowPos(NOACTIVATE) and then leaves the z-order alone for good (alt-tab and taskbar clicks stay the user's). show_library responses carry _foreground with what happened. kicad_install_symbol success now includes kicadParser (KiCad's verdict on the written file).

John Lauer ·0765c3656a ·1mo ago ·parent 6edee07
1 file changed +99
handlers/open_symbol_editor.py+99
@@ -327,6 +327,84 @@ def _find_kicad_manager() -> int | None:     return kicad_windows.find_project_manager()  +# ---------------------------------------------------------------------------+# Z-order guard for a NEW editor frame (John's rule, 2026-09-03; measured on+# ConfRoomROG: with the KiCad manager in front, the auto-rescan relaunch put the+# new Symbol Editor frame at z=0). Windows gives a freshly created top-level+# frame the top of the z-order; that is the OS, not the user. So:+#   1. before launching, remember which window the user had on top;+#   2. caption WHY a KiCad window is about to appear;+#   3. the moment the new frame exists, look ONCE: if it sits above the user's+#      window and that window is not KiCad's, slide the frame back behind it+#      with SetWindowPos(SWP_NOACTIVATE) on OUR window (bridge owns KiCad);+#   4. then hands off for good. If the user alt-tabs or clicks KiCad's taskbar+#      button a moment later, that is their choice and nothing here undoes it.+# ---------------------------------------------------------------------------++_LAST_FG_REPORT: dict = {}+++def _caption_new_frame(user_top: dict | None, rescan: bool = False) -> None:+    """John's rule: when you know a window will appear, say why, on screen, first."""+    try:+        from handlers import ad_client+        what = ("re-opening KiCad's Symbol Editor so it re-indexes the library (a new symbol was installed)"+                if rescan else "opening KiCad's Symbol Editor (it indexes every symbol library on open)")+        back = " It will be placed behind your current window." if user_top and not user_top.get("isKicad") else ""+        ad_client.call("desktop_caption", {"text": ("Adom Bridge: " + what + back)[:160], "seconds": 6,+                                           "reason": "Warn the user before a KiCad window appears on their screen"})+    except Exception:  # pylint: disable=broad-except+        pass+++def _user_top_window() -> dict | None:+    """The window at the top of the z-order right now, from AD's list+    (z field), or None when unreadable."""+    try:+        from handlers import kicad_windows+        from handlers import ad_client+        res = ad_client.call("desktop_list_windows", {})+        wins = (res or {}).get("windows") or []+        if not wins:+            return None+        top = sorted(wins, key=lambda w: w.get("z", 10**9))[0]+        img = str(top.get("image") or top.get("exe") or "").lower()+        return {"hwnd": int(top.get("hwnd") or 0), "title": top.get("title", ""),+                "isKicad": ("kicad" in img or "eeschema" in img or "pcbnew" in img)}+    except Exception:  # pylint: disable=broad-except+        return None+++def _guard_new_frame(new_hwnd: int, user_top: dict | None) -> dict:+    """One look, one SetWindowPos, then hands off. Returns a report for the response."""+    rep = {"userWindowBefore": (user_top or {}).get("hwnd"), "userTitleBefore": (user_top or {}).get("title"),+           "restored": False, "why": ""}+    if not new_hwnd or not user_top or not user_top.get("hwnd"):+        rep["why"] = "no reference window to give back (nothing was in front, or z-order unreadable)"+        return rep+    if user_top.get("isKicad"):+        rep["why"] = "the user's front window was already KiCad's; a new KiCad frame on top changes nothing for them"+        return rep+    if int(user_top["hwnd"]) == int(new_hwnd):+        rep["why"] = "the new frame IS the window that was in front"+        return rep+    now = _user_top_window()+    rep["topAfter"] = (now or {}).get("hwnd")+    if not now or int(now.get("hwnd") or 0) != int(new_hwnd):+        rep["why"] = "the new frame did not come to the top; nothing to restore"+        return rep+    try:+        SWP_NOSIZE, SWP_NOMOVE, SWP_NOACTIVATE = 0x0001, 0x0002, 0x0010+        ok = user32.SetWindowPos(int(new_hwnd), int(user_top["hwnd"]), 0, 0, 0, 0,+                                 SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE)+        rep["restored"] = bool(ok)+        rep["why"] = ("slid the new Symbol Editor frame behind the user's window (SetWindowPos, no activation); hands off from here"+                      if ok else "SetWindowPos refused; leaving the screen alone")+    except Exception as e:  # pylint: disable=broad-except+        rep["why"] = f"restore failed: {type(e).__name__}"+    return rep++ def _find_symbol_editor() -> int | None:     """Find an open Symbol Editor window."""     windows = _find_windows_by_title("Symbol Editor")@@ -886,6 +964,12 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:     sym_hwnd = _find_symbol_editor()     pathways_tried: list[str] = []     rescanned = False+    # Z-order guard (see _guard_new_frame): remember the user's front window and+    # say why a KiCad window is about to appear, BEFORE we launch anything.+    _user_top = None+    if not sym_hwnd:+        _user_top = _user_top_window()+        _caption_new_frame(_user_top)     _step("Symbol Editor already open" if sym_hwnd           else "opening the Symbol Editor (KiCad indexes every symbol library here)", 10) @@ -898,6 +982,8 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:         if _close_symbol_editor(sym_hwnd):             rescanned = True             sym_hwnd = 0+            _user_top = _user_top_window()+            _caption_new_frame(_user_top, rescan=True)             pathways_tried.append("rescan-closed-stale-editor")         else:             pathways_tried.append("rescan-close-failed")@@ -1101,6 +1187,15 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:         _step("Symbol Editor is up, letting it settle", 55)         time.sleep(1.5) +    # One look at the z-order the moment a frame WE launched exists, then hands off.+    global _LAST_FG_REPORT+    if _user_top is not None and sym_hwnd:+        _LAST_FG_REPORT = _guard_new_frame(sym_hwnd, _user_top)+    elif _user_top is not None:+        _LAST_FG_REPORT = {"restored": False, "why": "no editor frame appeared"}+    else:+        _LAST_FG_REPORT = {"restored": False, "why": "no window was launched by this call"}+     # --- Phase 2: Navigate to specific symbol (if requested) ---      if not symbol_name and not library_name:@@ -1165,6 +1260,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:             "output": f"Symbol '{search_text}' loaded (background win32 path): {_get_symbol_editor_title()}",             "via": "win32-background",             "navigation": dict(_LAST_NAV_TRACE),+            "_foreground": dict(_LAST_FG_REPORT),             "rescanned": rescanned,             **_resolved_fields(_get_symbol_editor_title(), library_name),         }@@ -1194,6 +1290,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:                       f"{_get_symbol_editor_title()}",             "via": "win32-background",             "navigation": dict(_LAST_NAV_TRACE),+            "_foreground": dict(_LAST_FG_REPORT),             "attempts": 2,             **_resolved_fields(_get_symbol_editor_title(), library_name),             "rescanned": rescanned,@@ -1281,11 +1378,13 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:                 # So report the paths, measured, and say only what they show.                 **_stale_explanation(kicad_info, search_text),                 "navigation": dict(_LAST_NAV_TRACE),+            "_foreground": dict(_LAST_FG_REPORT),             }         return {             "success": False, "editorOpened": True, "symbolLoaded": False,             "attempts": 2,             "navigation": dict(_LAST_NAV_TRACE),+            "_foreground": dict(_LAST_FG_REPORT),             "error": f"Symbol Editor open but '{search_text}' didn't load in the background "                      f"after two attempts (title: {_get_symbol_editor_title() or 'unknown'})",             "_hint": ("Two background attempts did not confirm, so a third is unlikely to "