← Commit history

0.9.287: Wiki #71 (Ray, 0.9.286 run): (1) the posted Enter now goes FIRST to the window holding keyboard focus in KiCad's GUI thread (GetGUIThreadInfo), and wxDataView children count as tree candidates regardless of height, so a short Libraries pane no longer yields zero targets; (2) a navigation that ran out of the call budget returns navigation_unconfirmed_within_budget with the trace and _foreground instead of the 'still coming up' contract about an editor that was already driven; (3) an editor the bridge opened after the library file was last written is never judged stale again, so repeated calls stop re-opening it (staleVeto says so); (4) symbol_editor_did_not_open carries pathwaysTried; navigation_failed responses carry errorCode and resolved fields.

John Lauer ·c30c23a49b ·1mo ago ·parent 0b55fb3
1 file changed +94−5
handlers/open_symbol_editor.py+94−5
@@ -342,6 +342,7 @@ def _find_kicad_manager() -> int | None: # ---------------------------------------------------------------------------  _LAST_FG_REPORT: dict = {}+_EDITOR_OPENED_AT: dict = {}  # hwnd -> epoch seconds when this module saw it appear   def _caption_new_frame(user_top: dict | None, rescan: bool = False) -> None:@@ -554,6 +555,29 @@ def _edit_text(hwnd: int) -> str:         return "?"  +def _gui_thread_focus(frame_hwnd: int) -> int | None:+    """hwnd holding keyboard focus inside the thread that owns `frame_hwnd`+    (GetGUIThreadInfo; cross-process, no focus change). None if unreadable."""+    try:+        tid = user32.GetWindowThreadProcessId(int(frame_hwnd), None)+        if not tid:+            return None++        class GUITHREADINFO(ctypes.Structure):+            _fields_ = [("cbSize", ctypes.wintypes.DWORD), ("flags", ctypes.wintypes.DWORD),+                        ("hwndActive", ctypes.wintypes.HWND), ("hwndFocus", ctypes.wintypes.HWND),+                        ("hwndCapture", ctypes.wintypes.HWND), ("hwndMenuOwner", ctypes.wintypes.HWND),+                        ("hwndMoveSize", ctypes.wintypes.HWND), ("hwndCaret", ctypes.wintypes.HWND),+                        ("rcCaret", ctypes.wintypes.RECT)]+        gti = GUITHREADINFO()+        gti.cbSize = ctypes.sizeof(GUITHREADINFO)+        if not user32.GetGUIThreadInfo(tid, ctypes.byref(gti)):+            return None+        return int(gti.hwndFocus) if gti.hwndFocus else None+    except Exception:  # pylint: disable=broad-except+        return None++ def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str,                                verify_lib: str = "") -> bool:     """v0.9.83 (#20/#21): plugin-free BACKGROUND symbol load. WM_SETTEXT into the@@ -608,6 +632,23 @@ def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str,         windows under the Libraries pane (wx generic controls share bland class         names, so geometry is the discriminator)."""         cands = []+        # Wiki #71 (Ray, 0.9.286): his screenshot showed the row SELECTED and+        # carrying the focus rectangle while the posted Enter never activated it.+        # The height>200 geometry filter below is DPI/layout dependent (a short+        # Libraries pane on a laptop yields ZERO candidates and Enter goes to+        # nobody). So ask Windows which child of KiCad's GUI thread has the+        # keyboard focus (GetGUIThreadInfo works across processes) and post+        # there FIRST; that is the exact window a human's Enter would reach.+        try:+            focus = _gui_thread_focus(sym_hwnd)+            if focus and focus != edit:+                fr = ctypes.wintypes.RECT()+                user32.GetWindowRect(focus, ctypes.byref(fr))+                cands.append((focus, fr))+                trace["focusHwnd"] = int(focus)+                trace["focusClass"] = _win_class(focus)+        except Exception:  # pylint: disable=broad-except+            pass         try:             pane = user32.GetParent(user32.GetParent(edit)) or user32.GetParent(edit)             WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND,@@ -615,7 +656,9 @@ def _background_navigate_win32(sym_hwnd: int, filter_text: str, verify: str,             def cb(hwnd, _l):                 r = ctypes.wintypes.RECT()                 user32.GetWindowRect(hwnd, ctypes.byref(r))-                if (r.bottom - r.top) > 200 and hwnd != edit:+                if hwnd == edit or any(h == hwnd for h, _ in cands):+                    return True+                if (r.bottom - r.top) > 200 or _win_class(hwnd) in ("wxDataView", "wxDataViewCtrl"):                     cands.append((hwnd, r))                 return True             cbp = WNDENUMPROC(cb)@@ -895,6 +938,10 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:             _existing = _find_symbol_editor()         except Exception:  # pylint: disable=broad-except             _existing = None+        try:+            _pt = list(pathways_tried)  # assigned later in the enclosing function; bound at call time+        except NameError:+            _pt = None         if not _existing:             return {                 "success": False,@@ -903,6 +950,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:                 "retryAfterMs": 5000,                 "phase": phase,                 "elapsedSec": int(time.monotonic() - _t0),+                "pathwaysTried": _pt,                 "error": ("No Symbol Editor window exists. The bridge asked KiCad to open one "                           "and none appeared, so this is not a slow cold start - nothing is "                           "coming up."),@@ -1191,6 +1239,7 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:     global _LAST_FG_REPORT     if _user_top is not None and sym_hwnd:         _LAST_FG_REPORT = _guard_new_frame(sym_hwnd, _user_top)+        _EDITOR_OPENED_AT[int(sym_hwnd)] = time.time()     elif _user_top is not None:         _LAST_FG_REPORT = {"restored": False, "why": "no editor frame appeared"}     else:@@ -1278,10 +1327,30 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:     # The first attempt is what warms the tree, so this is a fresh-tree retry,     # not a duplicate of a hopeless call.     if _out_of_budget():-        # #54: the editor is up but a cold index made the first navigate slow;-        # return the starting contract rather than risk dying mid-retry - the-        # caller's next call navigates a warm tree in seconds.-        return _starting_contract("navigate-retry-on-cold-index")+        # Wiki #71 (Ray, 0.9.286): this used to return the "still coming up"+        # contract, which threw away a navigation that had ALREADY run against+        # an editor that was up (his screenshot: filter typed, row selected and+        # focused). "Starting" is not what happened. Say what did, with the+        # trace, and let the caller re-call a warm editor.+        return {+            "success": False,+            "errorCode": "navigation_unconfirmed_within_budget",+            "retryable": True,+            "retryAfterMs": 3000,+            "elapsedSec": int(time.monotonic() - _t0),+            "editorOpened": True,+            "rescanned": rescanned,+            "navigation": dict(_LAST_NAV_TRACE),+            "_foreground": dict(_LAST_FG_REPORT),+            **_resolved_fields(_get_symbol_editor_title(), library_name),+            "error": (f"The Symbol Editor is open and one background navigation ran, but the title did not "+                      f"confirm '{search_text}' before this call's time budget ended "+                      f"(title: {_get_symbol_editor_title() or 'unknown'})."),+            "_hint": ("Re-call with the SAME args: the editor is warm now, so the whole call is the "+                      "navigation alone (seconds). Read navigation.steps and navigation.treeCandidates: "+                      "zero candidates means Enter reached no window (report it with the trace); "+                      "focusHwnd is where a human's Enter would land."),+        }     _step(f"first pass did not confirm; retrying now the tree is warm", 90)     if _background_navigate_win32(sym_hwnd, _filter, _verify, library_name):         return {@@ -1317,6 +1386,23 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:                           "a bridge bug worth reporting, not a library problem."),             }         stale = _symbol_on_disk_but_not_in_editor(kicad_info, symbol_name or search_text)+        # Wiki #71 (Ray, 0.9.286): three identical calls each re-opened the editor+        # and blew the budget, because "symbol on disk + editor exists" read as+        # stale EVERY time. An editor this module opened AFTER the library file+        # was last written has already indexed that symbol; a failed navigation+        # against it is a navigation problem, and reopening cannot help.+        _stale_veto = ""+        try:+            _opened = _EDITOR_OPENED_AT.get(int(sym_hwnd or 0))+            if stale and _opened:+                import adom_library as _al+                _paths = _al._get_paths(kicad_info)+                if _paths and Path(_paths[1]).exists() and Path(_paths[1]).stat().st_mtime < _opened:+                    stale = False+                    _stale_veto = ("the editor was opened by the bridge after the library file was last "+                                   "written, so its index already holds the symbol; reopening would not help")+        except Exception:  # pylint: disable=broad-except+            pass          # 0.9.211 (wiki #35, Colby's root cause). We KNOW the remedy here, so         # handing it back as a required argument is passing our own retry out@@ -1385,6 +1471,9 @@ def _handle_open_symbol_editor_inner(kicad_info: dict, args: dict) -> dict:             "attempts": 2,             "navigation": dict(_LAST_NAV_TRACE),             "_foreground": dict(_LAST_FG_REPORT),+            "errorCode": "navigation_failed",+            "staleVeto": _stale_veto or None,+            **_resolved_fields(_get_symbol_editor_title(), library_name),             "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 "