← Commit history

v0.9.106: chords resolve via the menu-bar accelerator table -> WM_COMMAND (background); Alt+3 keystroke fallbacks deleted

John Lauer ·82dee790d4 ·2mo ago ·parent 6544271
8 files changed +85−65
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.105+0.9.106
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.105",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.105/adom-bridge-kicad-v0.9.105.zip",-  "sha256": "20642cf4c35bc8dacb55568f86c0de5fec951cc0d212ddc29b804180b8174a8c",-  "size": 396469,+  "version": "0.9.106",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.106/adom-bridge-kicad-v0.9.106.zip",+  "sha256": "c0a6ab963c2544b796b32184f211d8decddb7786df984994ca91f92362253d98",+  "size": 396973,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-15T10:09:52Z",+  "released_at": "2026-08-15T10:18:25Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.106.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.105",+  "version": "0.9.106",   "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/kicad_ui.py+20−1
@@ -581,9 +581,28 @@ def handle_send_key(kicad_info: dict, args: dict) -> dict:     # theft, works while the window is buried. Chords still need the focus path     # (posted messages don't set global modifier state), so those are gated.     combo_probe = _parse_modifier_combo(key)+    if combo_probe is not None:+        # v0.9.106 (John): a CHORD usually IS a menu accelerator — resolve it from+        # the window's Win32 menu bar and fire PostMessage(WM_COMMAND). Fully+        # background: no foreground, no real keystrokes, no fragility. Only when+        # no menu item carries the accelerator do we fall to the foreground path.+        try:+            from handlers import win_menu+            cid, label = win_menu.find_by_accelerator(target_hwnd, key)+            if cid:+                win_menu.invoke_menu_command(target_hwnd, cid)+                return {"success": True,+                        "output": f"Chord '{key}' delivered as menu command '{label}' (background)",+                        "data": {"method": "menu-command", "cmdId": cid, "menuItem": label,+                                 "hwnd": target_hwnd, "title": title, "foreground": False},+                        "_hint": ("Resolved via the window's menu-bar accelerator table and fired "+                                  "with WM_COMMAND — no focus was touched. This is the preferred "+                                  "path for every chord that maps to a menu item.")}+        except Exception:+            pass     if not allow_fg:         if combo_probe is not None:-            return _foreground_warning(f"send the key chord '{key}' to KiCad")+            return _foreground_warning(f"send the key chord '{key}' to KiCad — no menu item carries this accelerator, so only real (focused) keystrokes can deliver it")         vk = VK_MAP.get(key)         ch = key if (vk is None and len(key) == 1) else None         if vk is None and ch:
handlers/open_footprint_editor.py+7−56
@@ -944,35 +944,8 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict:         except Exception as e:  # pylint: disable=broad-except             fp_pathways.append(f"uia-error:{type(e).__name__}") -        # Opt-in disruptive fallback: Alt+3 keystroke (needs the FP window foregrounded,-        # so it steals focus) — only when the caller explicitly allows it.-        if args.get("allowFocusSteal"):-            try:-                from handlers import win_focus as _wf-                _wf.announce_foreground("open the 3D viewer via Alt+3")-            except Exception:-                pass-            VK_MENU_LOCAL = 0x12-            VK_3 = 0x33-            user32.SetForegroundWindow(fp_hwnd)-            time.sleep(0.2)-            try:-                user32.keybd_event(VK_MENU_LOCAL, 0, 0, 0)-                user32.keybd_event(VK_3, 0, 0, 0)-                user32.keybd_event(VK_3, 0, KEYEVENTF_KEYUP, 0)-            finally:-                user32.keybd_event(VK_MENU_LOCAL, 0, KEYEVENTF_KEYUP, 0)-            fp_pathways.append("alt3_focussteal")-            time.sleep(2.5)-            windows = _find_windows_by_title("3D Viewer")-            if windows:-                return {-                    "success": True,-                    "output": f"3D Viewer opened from Footprint Editor: {windows[0][1]}",-                    "pathway": "fp_alt3",-                    "pathwaysTried": fp_pathways,-                }-+        # (v0.9.106: the Alt+3 keystroke fallback is DELETED — the Win32 menu+        # Tier 0 above delivers the same command by PostMessage, background.)         # FP path failed (plugin frame absent AND no focus-steal fallback).         # If caller asked for 'auto' AND a PCB editor is also open, fall through         # to PCB attempt rather than dead-ending here.@@ -1060,33 +1033,11 @@ def handle_open_3d_viewer(kicad_info: dict, args: dict) -> dict:         # keystroke lands there (a global Alt+3 to the user's own window could fire         # their app's Alt+3). That steals focus, so it's OPT-IN only         # ({"allowFocusSteal": true}); by default we fail to the background path.-        if args.get("allowFocusSteal"):-            try:-                from handlers import win_focus as _wf-                _wf.announce_foreground("open the board 3D viewer via Alt+3")-            except Exception:-                pass-            user32.SetForegroundWindow(pcb_hwnd)-            time.sleep(0.3)-            VK_MENU = 0x12   # Alt key-            VK_3 = 0x33      # '3' key-            KEYEVENTF_KEYUP = 0x0002-            user32.keybd_event(VK_MENU, 0, 0, 0)           # Alt down-            user32.keybd_event(VK_3, 0, 0, 0)               # 3 down-            user32.keybd_event(VK_3, 0, KEYEVENTF_KEYUP, 0) # 3 up-            user32.keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0) # Alt up-            pathways_tried.append("alt_keystroke")-            time.sleep(3.0)-            windows = _find_windows_by_title("3D Viewer")-            if windows:-                return {-                    "success": True,-                    "output": f"3D Viewer opened from PCB Editor: {windows[0][1]}",-                    "pathway": "alt_keystroke",-                    "pathwaysTried": pathways_tried,-                }-        else:-            pathways_tried.append("alt_keystroke_skipped_background_mode")+        # (v0.9.106: pcb Alt+3 keystroke fallback DELETED — win32-menu Tier 0+        # delivers 3D Viewer via WM_COMMAND, background.)+        if False:+            pass+          return {             "success": False,
handlers/win_menu.py+50
@@ -79,3 +79,53 @@ def open_via_menu(hwnd: int, *contains: str):         return {"ok": False, "reason": info}     ok = invoke_menu_command(hwnd, cid)     return {"ok": ok, "cmdId": cid, "menuText": info}+++def _norm_accel(text: str) -> str:+    """Normalize an accelerator for comparison: 'Ctrl+Shift+S' -> 'ctrl+shift+s'."""+    return "".join((text or "").lower().split())+++def find_by_accelerator(hwnd: int, chord: str, max_depth: int = 4):+    """Resolve a keyboard CHORD (e.g. 'ctrl+s', 'alt+3') to the menu command that+    carries it as an accelerator — wx menu items render as 'Save\tCtrl+S', so the+    menu bar doubles as a chord->command table. Returns (cmd_id, menu_text) or+    (None, reason). This lets send_key deliver chords via PostMessage(WM_COMMAND)+    — fully background — instead of real keystrokes that need the foreground+    (John 2026-08-15: use menu navigation instead of send_key)."""+    if not IS_WINDOWS or not hwnd:+        return None, "no-win32"+    user32.GetMenu.restype = ctypes.c_void_p+    user32.GetSubMenu.restype = ctypes.c_void_p+    hmenu = user32.GetMenu(ctypes.c_void_p(hwnd))+    if not hmenu:+        return None, "no-menu"+    want = _norm_accel(chord)++    def _text(menu, i):+        buf = ctypes.create_unicode_buffer(256)+        user32.GetMenuStringW(ctypes.c_void_p(menu), i, buf, 256, MF_BYPOSITION)+        return buf.value or ""++    def _walk(menu, depth):+        n = user32.GetMenuItemCount(ctypes.c_void_p(menu))+        for i in range(max(0, n)):+            raw = _text(menu, i)+            sub = user32.GetSubMenu(ctypes.c_void_p(menu), i)+            if sub:+                if depth < max_depth:+                    hit = _walk(sub, depth + 1)+                    if hit is not None:+                        return hit+            elif "\t" in raw:+                label, accel = raw.split("\t", 1)+                if _norm_accel(accel) == want:+                    cid = user32.GetMenuItemID(ctypes.c_void_p(menu), i)+                    if cid and cid != -1:+                        return cid, label.replace("&", "")+        return None++    hit = _walk(hmenu, 0)+    if hit is None:+        return None, "no-accelerator-match"+    return hit
server.py+1−1
@@ -310,7 +310,7 @@ _VERB_CATALOG = {     "screenshot_all": {"summary": "Screenshot every open KiCad window.", "long": False,         "hint": "Captures all KiCad windows; the go-to way to VERIFY what actually happened after an open/edit.",         "related": ["kicad_open_editors", "kicad_window_info", "kicad_open_board"], "pitfalls": ["a window still rendering (e.g. 3D viewer) shows blank — wait a beat first"]},-    "send_key": {"summary": "Send a keystroke to an EXACT KiCad window by hwnd (SendMessage; not for chords).", "long": False,+    "send_key": {"summary": "Send a key to KiCad — chords auto-resolve to menu commands (background)", "long": False,         "hint": "For modifier CHORDS ('ctrl+s', 'alt+3', 'ctrl+shift+p') use Bridge's desktop_press_key — it owns full chord parsing; don't route chords here. Use this verb only for the case Bridge's focus-first model can't cover: a single key delivered to an EXACT hwnd (e.g. Enter/Escape to a specific dialog from window_info).",         "related": ["kicad_window_info", "kicad_click", "kicad_fix_keyboard"], "pitfalls": ["not for chords — use desktop_press_key", "needs a target hwnd (from window_info)"]},     "click": {"summary": "Click at coordinates inside a KiCad window.", "long": False,