← Commit history

v0.9.74: plugin 0.9.1 two-hop background load (filter -> settle 0.8s on worker thread -> activate; fixed DataViewEvent 3-arg ctor) + open_symbol_editor tries the plugin path first, PS nav as fallback

John Lauer ·83c1f24f73 ·2mo ago ·parent 7a10a0d
7 files changed +99−22
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.73+0.9.74
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.73",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.73/adom-bridge-kicad-v0.9.73.zip",-  "sha256": "b62a3811b19bcfd1b30db9ccb9463329e8931417ea05da7609cb55c8f5907ddb",-  "size": 366949,+  "version": "0.9.74",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.74/adom-bridge-kicad-v0.9.74.zip",+  "sha256": "968d98586a21f4221d0900c2b117f0f032c7ec8c44be79afd2780a3a3dd34cec",+  "size": 367941,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-09T01:40:12Z",+  "released_at": "2026-08-09T01:47:18Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.74.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.73",+  "version": "0.9.74",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/open_symbol_editor.py+68−7
@@ -125,11 +125,50 @@ def _find_child_by_class_and_parent(parent_hwnd: int, child_class: str, parent_t     return result[0]  +SW_SHOWNOACTIVATE = 4++ def _force_foreground(hwnd: int):-    """Bring a window to the foreground."""-    user32.ShowWindow(hwnd, SW_RESTORE)-    user32.SetForegroundWindow(hwnd)-    user32.BringWindowToTop(hwnd)+    """DELIBERATELY does NOT foreground — the bridge must never steal the user's+    focus (John, 2026-07). Just make the window visible without activating it; the+    plugin WM_COMMAND path drives it without focus and the dispatcher restores the+    user's window afterward. Name kept to avoid churning the call sites."""+    try:+        user32.ShowWindow(hwnd, SW_SHOWNOACTIVATE)+    except Exception:+        pass+++def _get_symbol_editor_title() -> str:+    """Current Symbol Editor window title ('' if not found) — load ground truth."""+    try:+        hwnd = _find_symbol_editor()+        if not hwnd:+            return ""+        buf = ctypes.create_unicode_buffer(512)+        user32.GetWindowTextW(hwnd, buf, 512)+        return buf.value or ""+    except Exception:  # pylint: disable=broad-except+        return ""+++def _plugin_background_load(symbol_name: str) -> dict:+    """v0.9.74: background canvas-load via the in-process plugin (payload 0.9.1+).++    Two-hop filter->settle->activate inside the plugin, zero focus/keyboard+    theft (John's no-foreground mandate). Uses bridge_client's discovery to find+    the live eeschema instance. {'loaded': False, ...} on any failure so the+    caller can fall through to the legacy paths.+    """+    try:+        from .bridge_client import _resolve_plugin, call as _plugin_call+        info, err = _resolve_plugin("eeschema")+        if err is not None or not info:+            return {"loaded": False, "_error": "no live eeschema plugin"}+        return _plugin_call(info["port"], "select_and_load_symbol",+                            {"symbolName": symbol_name}, timeout=15.0) or {"loaded": False}+    except Exception as e:  # pylint: disable=broad-except+        return {"loaded": False, "_error": f"{type(e).__name__}: {e}"}   def _navigate_to_symbol(sym_hwnd: int, symbol_name: str) -> dict:@@ -292,7 +331,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:                 else f"plugin-failed:{tier1.get('errorCode','')}"             )             if tier1.get("success"):-                for _ in range(20):+                for _ in range(12):                     time.sleep(0.5)                     sym_hwnd = _find_symbol_editor()                     if sym_hwnd:@@ -329,8 +368,15 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:                 }              try:+                _si = None+                try:+                    from handlers import win_focus+                    _si = win_focus.background_startupinfo()  # open the manager behind the user+                except Exception:+                    _si = None                 subprocess.Popen(                     [kicad_exe],+                    startupinfo=_si,                     stdout=subprocess.DEVNULL,                     stderr=subprocess.DEVNULL,                 )@@ -341,7 +387,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:                     "_hint": "Call close_kicad with {\"force\": true} to clear stuck processes, then retry.",                 } -            for _ in range(20):+            for _ in range(12):                 time.sleep(0.5)                 kicad_hwnd = _find_kicad_manager()                 if kicad_hwnd:@@ -359,7 +405,7 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:         user32.PostMessageW(kicad_hwnd, WM_COMMAND, KICAD_SYMBOL_EDITOR_CMD_ID, 0)         pathways_tried.append("wm_command") -        for _ in range(20):+        for _ in range(12):             time.sleep(0.5)             sym_hwnd = _find_symbol_editor()             if sym_hwnd:@@ -395,6 +441,21 @@ def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:     # users who want to experiment, but the default kicad_open_symbol_editor     # flow keeps using the proven PowerShell path. +    # v0.9.74: FIRST try the plugin's background canvas-load (payload 0.9.1's+    # two-hop filter->settle->activate) — zero focus/keyboard theft. The PS+    # navigate below is the fallback when no plugin instance is alive.+    _pl = _plugin_background_load(search_text)+    if str(_pl.get("loaded")) in ("True", "unverified"):+        time.sleep(0.6)+        _title = _get_symbol_editor_title() if "_get_symbol_editor_title" in globals() else ""+        if _pl.get("loaded") is True or (search_text.lower() in (_title or "").lower()):+            return {+                "success": True,+                "output": f"Symbol '{search_text}' loaded (background plugin path): {_title or _pl.get('frameTitle','')}",+                "via": "plugin-background",+            }+        # unverified and title doesn't confirm — fall through to PS nav+     nav_result = _navigate_to_symbol(sym_hwnd, search_text)      if nav_result["status"] == "ok":
plugin_install.py+1−1
@@ -49,7 +49,7 @@ from typing import Optional # Bridge version we're shipping. Bumped together with adom_bridge.py's
 # __version__ — version drift between this file and the payload would
 # cause an infinite reinstall loop; the runtime asserts they match.
-PAYLOAD_VERSION = "0.9.0"
+PAYLOAD_VERSION = "0.9.1"
 
 # Module-level cache: install runs at most ONCE per bridge process boot.
 # Multiple concurrent kicad_* commands hitting the bridge during startup
plugin_payload/adom_bridge.py+23−7
@@ -60,7 +60,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any, Callable, Optional -__version__ = "0.9.0"+__version__ = "0.9.1"  # Exe-name -> port offset. The KiCad bridge in adom-desktop knows this # table too. New exes get appended; never renumber existing entries or@@ -2164,13 +2164,11 @@ def _ui_select_and_load(editor_kind: str, name: str, use_send_input: bool = Fals                     sel_ok = bool(sel and sel.IsOk())                     transport_detail["selection"] = sel_ok                     if sel_ok:+                        # wxPython overloads: (type, ctrl, item) is the one that+                        # builds a fully-populated event (the no-arg + SetItem+                        # pattern raises TypeError on 4.2).                         evt = wx.dataview.DataViewEvent(-                            wx.dataview.wxEVT_DATAVIEW_ITEM_ACTIVATED)-                        try:-                            evt.SetEventObject(dvc)-                            evt.SetItem(sel)-                        except Exception:  # pylint: disable=broad-except-                            pass+                            wx.dataview.wxEVT_DATAVIEW_ITEM_ACTIVATED, dvc, sel)                         wx.PostEvent(dvc, evt)                         keys_sent.append("DATAVIEW_ITEM_ACTIVATED(selection)")                 except Exception as e:  # pylint: disable=broad-except@@ -2881,8 +2879,26 @@ def _dispatch(method: str, params: dict) -> Any:     if method == "select_and_load_symbol":         name = params.get("symbolName") or params.get("name") or ""         use_si = bool(params.get("useSendInput"))+        if not use_si:+            # Two-hop background path (v0.9.1): KiCad's LIB_TREE debounces the+            # search filter (~200-500ms on a timer that runs on the UI thread),+            # so filter-then-activate in ONE UI call always sees an empty+            # selection (verified live: first call selection:false, second call+            # loads). We're on the RPC worker thread here, so hop 1 sets the+            # filter, we sleep while the UI thread's debounce fires, then hop 2+            # activates the pre-selected match.+            import time as _t+            _call_on_ui_thread(lambda: _ui_navigate_symbol(name))+            _t.sleep(0.8)+            return _call_on_ui_thread(lambda: _ui_select_and_load("symbol", name, use_send_input=False))         return _call_on_ui_thread(lambda: _ui_select_and_load_symbol(name, use_si), timeout=30.0)     if method == "select_and_load_footprint":+        _n = params.get("footprintName") or params.get("name") or ""+        if not bool(params.get("useSendInput")):+            import time as _t+            _call_on_ui_thread(lambda: _ui_navigate_footprint(_n))+            _t.sleep(0.8)+            return _call_on_ui_thread(lambda: _ui_select_and_load("footprint", _n, use_send_input=False))         name = params.get("footprintName") or params.get("name") or ""         use_si = bool(params.get("useSendInput"))         return _call_on_ui_thread(lambda: _ui_select_and_load_footprint(name, use_si), timeout=30.0)