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
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).
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 "