← Commit history

1.0.42: 1.0.42: two switches the user can flip from the dashboard, both applied at once with no restart. backgroundGuard (default on) is the A/B switch John asked for: off means the bridge never pushes or moves any KiCad window, and the focus log still records what it WOULD have done (guard OFF: would have pushed this window) so the two modes can be compared. activityIndicator finally does something in the native build: Windows flashes a taskbar button orange whenever an app asks for the foreground and is refused or is sent behind right after taking it, which is every KiCad window the bridge opens in the background; progress and none now cancel that flash on bridge-owned windows (about once a second and right after a push), flash and both leave it. The Python build did this; the port dropped it, so the setting was dead from 1.0.0 (John: whackamole). Still not ported: the taskbar progress bar and the Adom overlay badge (wiki #113).

John Lauer ·c27149e66b ·1mo ago ·parent d337d1a
4 files changed +71
rust/crates/kicad-core/src/settings.rs+4
@@ -25,6 +25,10 @@ pub fn defaults() -> Vec<(&'static str, Value)> {         // on-screen caption on the driven box. 0 turns the toast off (John, 2026-09-19:         // one per verb "makes them more disruptive than you foregrounding kicad").         ("foregroundToastMinutes", json!(10)),+        // The A/B switch John asked for (2026-09-21): true = a KiCad window the bridge is opening is+        // sent behind the user's window; false = the bridge never moves or pushes any KiCad window,+        // so whatever KiCad does with the foreground is what the user sees.+        ("backgroundGuard", json!(true)),     ] } 
rust/crates/kicad-platform/src/lib.rs+7
@@ -226,6 +226,13 @@ pub trait Platform: Sync + Send {      /// Start the sentinel and event-hook threads once. Idempotent.     fn etiquette_start(&self) {}+    /// Turn the "send a KiCad window the bridge just opened behind the user's window" guard on or+    /// off (setting `backgroundGuard`). Off means KiCad windows are never moved or pushed by the+    /// bridge at all; events are still logged so the two modes can be compared.+    fn etiquette_set_enabled(&self, _on: bool) {}+    /// Cancel Windows' taskbar attention flash on bridge-owned KiCad windows (setting+    /// `activityIndicator`: progress and none suppress it, flash and both leave it).+    fn etiquette_set_flash_suppressed(&self, _on: bool) {}     /// A window verb begins (arms the guard, snapshots the park baseline). `spawns` = the     /// verb opens a new window and gets the long guard.     fn etiquette_begin_verb(&self, _verb: &str, _spawns: bool) {}
rust/crates/kicad-platform/src/win/etiquette.rs+45
@@ -687,6 +687,17 @@ fn handle_foreground(h: u64, source: Source, cache: &mut ExeCache) {         (v, t)     };     let title = title_nohang(hwnd(h));+    // Guard switched off by the user: decide as usual so the log still says what WOULD have+    // happened (that is the point of an A/B), but touch nothing.+    let verdict = match verdict {+        Verdict::Bounce(_) if !GUARD_ENABLED.load(std::sync::atomic::Ordering::Relaxed) => {+            let mut g = lock();+            g.backgrounded_once.remove(&(h, pid));+            g.bounces = g.bounces.saturating_sub(1);+            Verdict::Leave("guard OFF: would have pushed this window (left alone)")+        }+        v => v,+    };     let action: String = match verdict {         Verdict::Ignore => return,         Verdict::Leave(label) => label.to_string(),@@ -707,6 +718,9 @@ fn handle_foreground(h: u64, source: Source, cache: &mut ExeCache) {             if BOUNCE_HOLD_NOACTIVATE {                 suppress_activation(h);             }+            if SUPPRESS_FLASH.load(std::sync::atomic::Ordering::Relaxed) {+                focus::stop_flash(hwnd(h));+            }             // 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;@@ -871,6 +885,7 @@ fn sentinel_loop() {     let mut cache = ExeCache::default();     let mut cache_born = Instant::now();     let mut booted = false;+    let mut flash_tick: u32 = 0;     loop {         let now = Instant::now();         if cache_born.elapsed() > Duration::from_secs(5) {@@ -917,6 +932,15 @@ fn sentinel_loop() {                 log(&format!("[focus] bridge idle: cleared activation block on {n} window(s)"));             }         }+        // 1b. Attention flash: about once a second, clear it on the windows the bridge owns.+        flash_tick = flash_tick.wrapping_add(1);+        if flash_tick % 8 == 0 && SUPPRESS_FLASH.load(std::sync::atomic::Ordering::Relaxed) {+            for h in kicad_hwnds(&mut cache) {+                if is_owned_window(h) {+                    focus::stop_flash(hwnd(h));+                }+            }+        }         // 2. The transition.         let fg = focus::foreground();         // NEVER a shell window. Clicking a taskbar button makes the taskbar the foreground window for@@ -1140,6 +1164,27 @@ pub fn sanction(h: u64, seconds: f64) {     g.sanctioned_hwnd = h; } +/// Cancel Windows' attention flash on bridge-owned KiCad windows (setting `activityIndicator`:+/// "progress" and "none" suppress it, "flash" and "both" leave it alone).+static SUPPRESS_FLASH: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);++pub fn set_flash_suppressed(on: bool) {+    SUPPRESS_FLASH.store(on, std::sync::atomic::Ordering::Relaxed);+}++/// The user's A/B switch (setting `backgroundGuard`, the dashboard's "Keep KiCad in the background").+static GUARD_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);++pub fn set_enabled(on: bool) {+    let was = GUARD_ENABLED.swap(on, std::sync::atomic::Ordering::Relaxed);+    if was != on {+        log(&format!("[focus] background guard switched {}", if on { "ON" } else { "OFF: KiCad windows are never pushed" }));+        if !on {+            allow_activation_all(); // nothing the guard ever set may outlive it+        }+    }+}+ pub fn events() -> Vec<Value> {     lock().events.clone() }
rust/crates/kicad-platform/src/win/focus.rs+15
@@ -54,6 +54,21 @@ pub fn show_no_activate(h: HWND) -> Result<(), String> { /// GetForegroundWindow reports the target afterwards, not whether the calls returned. /// Minimized windows are restored; anything else keeps its size (SW_RESTORE would /// un-maximize a maximized editor, which the Python version got wrong).+/// Cancel Windows' orange attention flash on a taskbar button (FLASHW_STOP). Windows starts that+/// flash by itself whenever an app asks for the foreground and is refused, or is sent behind right+/// after taking it, which is what happens to every KiCad window the bridge opens in the background.+/// The Python build cancelled it (handlers/win_taskbar.py); the native build never did until 1.0.42,+/// so the user had to click each button to stop it (John: "whackamole").+pub fn stop_flash(h: HWND) {+    use windows::Win32::UI::WindowsAndMessaging::{FlashWindowEx, FLASHWINFO, FLASHW_STOP};+    if h.is_invalid() {+        return;+    }+    let fi = FLASHWINFO { cbSize: std::mem::size_of::<FLASHWINFO>() as u32, hwnd: h, dwFlags: FLASHW_STOP, uCount: 0, dwTimeout: 0 };+    // SAFETY: a fully initialised FLASHWINFO for a window handle; the call only clears a flag.+    let _ = unsafe { FlashWindowEx(&fi) };+}+ /// Ask Windows to make `h` the foreground window: one SetForegroundWindow and nothing else. No /// AttachThreadInput, no ShowWindow, no BringWindowToTop. Returns whether Windows agreed. pub fn request_foreground(h: HWND) -> bool {