← Commit history

1.0.40: 1.0.40: the bridge no longer crashes Windows Explorer. When a KiCad window reached the foreground during a verb, the bridge sent it behind and then forced the user's previous window back to the front (thread-input attach, ShowWindow, BringWindowToTop, SetForegroundWindow). Clicking a KiCad taskbar button makes the TASKBAR that previous window, so the bridge did this to the Windows 11 taskbar and Explorer crashed inside Taskbar.View.dll (0xc0000005), twice in a row on AdomLapper and earlier on winvm. Now: a shell window (taskbar, Start, desktop, anything owned by explorer.exe) is never recorded as the user's window and never forced to the front. And the user's own click is recognised properly: any input in the last 1.5 s with the cursor on the taskbar or on the KiCad window means the user brought it up, so it is left alone; the old test polled the mouse button every 120 ms and missed quick clicks, which is why a clicked KiCad window was sometimes sent away again. This bug is in every earlier native build, including public 1.0.37.

John Lauer ·28a71c69a3 ·1mo ago ·parent 30bbdda
1 file changed +29−3
rust/crates/kicad-platform/src/win/etiquette.rs+29−3
@@ -67,6 +67,8 @@ use super::{focus, hwnd, hwnd_u64, is_kicad_exe, is_window, process, responsive}  // ── Tunables, every one of them a number the Python carried ────────────────── +/// User input this recent, with the cursor on the shell or on the window, is the user's own click.+const USER_INPUT_GRACE_SECS: f64 = 1.5; /// Sentinel poll period. const POLL: Duration = Duration::from_millis(120); /// A click counts as "the user brought this up" for this long after it landed.@@ -471,6 +473,19 @@ fn cursor_over(h: u64) -> bool {     !root.is_invalid() && hwnd_u64(root) == h } +/// A window that belongs to the Windows shell: by class, or because explorer.exe owns it. The bridge+/// never forces one of these to the foreground and never treats one as "the user's window".+fn is_shell_window(h: u64, cache: &mut ExeCache) -> bool {+    if h == 0 {+        return false;+    }+    if SHELL_CLASSES.contains(&class_name(hwnd(h)).as_str()) {+        return true;+    }+    let exe = cache.exe(pid_of(hwnd(h))).to_lowercase();+    exe.ends_with("explorer.exe") || exe.ends_with("shellexperiencehost.exe") || exe.ends_with("startmenuexperiencehost.exe") || exe.ends_with("searchhost.exe")+}+ fn cursor_over_shell() -> bool {     let root = cursor_root();     !root.is_invalid() && SHELL_CLASSES.contains(&class_name(root).as_str())@@ -624,7 +639,11 @@ fn handle_foreground(h: u64, source: Source, cache: &mut ExeCache) {         let mut g = lock();         sample_input(&mut g, now);         expire_guards(&mut g, now);-        let clicked = within(g.last_click, CLICK_WINDOW, now);+        // A click is the polled button state (120 ms sentinel, so a quick click is missed and the+        // user's own window was bounced "sometimes") OR any user input in the last 1.5 s. Either one+        // only counts with the cursor on the shell (a taskbar button, Start) or on this window:+        // someone typing in their editor while KiCad raises itself has the cursor elsewhere.+        let clicked = within(g.last_click, CLICK_WINDOW, now) || ago < USER_INPUT_GRACE_SECS;         // Only pay for the hit tests when a click is recent.         let clicked_here = clicked && (cursor_over(h) || cursor_over_shell());         let t = Transition {@@ -674,7 +693,8 @@ fn handle_foreground(h: u64, source: Source, cache: &mut ExeCache) {             // Hand the activation back to the user's window (restoring what they had);             // without this KiCad keeps the activation and raises itself again.             let user = lock().last_user_fg;-            let restored = user != 0 && user != h && matches!(focus::bring_to_front(hwnd(user)), Ok(true));+            let shell = user != 0 && is_shell_window(user, &mut ExeCache::default());+            let restored = user != 0 && user != h && !shell && matches!(focus::bring_to_front(hwnd(user)), Ok(true));             lock().bounce_holds.insert(h, now + BOUNCE_HOLD);             log(&format!("[bg] {h}: sent behind ({result}); user window {user} {}.", if restored { "restored" } else { "not restored" }));             match source {@@ -878,7 +898,13 @@ fn sentinel_loop() {         }         // 2. The transition.         let fg = focus::foreground();-        if fg != 0 && !is_kicad_exe(&exe_of(fg, &mut cache)) {+        // NEVER a shell window. Clicking a taskbar button makes the taskbar the foreground window for+        // a moment; recording it here meant the next bounce "restored" it with bring_to_front, which+        // attaches to the target's input thread and calls ShowWindow / BringWindowToTop /+        // SetForegroundWindow on it. Done to the Windows 11 taskbar that crashed Explorer inside+        // Taskbar.View.dll (0xc0000005), twice in a row, the moment John clicked a KiCad taskbar+        // button (AdomLapper, 2026-09-21; the same thing killed Explorer on winvm the night before).+        if fg != 0 && !is_kicad_exe(&exe_of(fg, &mut cache)) && !is_shell_window(fg, &mut cache) {             lock().last_user_fg = fg;         }         if fg != last_fg {