app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
rust: Windows window-control layer (enumerate, process, messages, menu, focus, capture, UIA), all cross-process calls h…
3ecdded
2mo ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
//! The native menu bar: walk it, read ids, fire WM_COMMAND. Plugin-free and focus-free
//! (issue #26). wxFrame menus on Windows are real Win32 menus attached with SetMenu, so
//! GetMenu / GetSubMenu / GetMenuStringW / GetMenuItemID read them from any process, and
//! ids resolved from the live bar are version-proof where hardcoded ids were not.
//! Python: win_menu.py `find_menu_command`, `invoke_menu_command`, `find_by_accelerator`.
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{GetMenu, GetMenuItemCount, GetMenuItemID, GetMenuStringW, GetSubMenu, HMENU, MF_BYPOSITION, WM_COMMAND};
use super::messages::post;
use super::{from_wide, hwnd_u64, strip_amp};
use crate::MenuItem;
/// Deepest submenu level walked (win_menu.py `max_depth`).
const MAX_DEPTH: usize = 4;
fn item_text(menu: HMENU, pos: u32) -> String {
let mut buf = [0u16; 256];
// SAFETY: buffer passed with its length; MF_BYPOSITION means `pos` is an index.
let n = unsafe { GetMenuStringW(menu, pos, Some(&mut buf), MF_BYPOSITION) };
from_wide(&buf[..n.max(0) as usize])
}
/// Split "Save\tCtrl+S" into ("Save", Some("Ctrl+S")), mnemonics stripped.
pub fn split_caption(raw: &str) -> (String, Option<String>) {
match raw.split_once('\t') {
Some((label, accel)) => (strip_amp(label).trim().to_string(), Some(accel.trim().to_string()).filter(|a| !a.is_empty())),
None => (strip_amp(raw).trim().to_string(), None),
}
}
fn walk(menu: HMENU, depth: usize, path: &mut Vec<String>, out: &mut Vec<MenuItem>) {
// SAFETY: a menu handle read from another process's window is still a valid global
// user object for these read-only queries.
let n = unsafe { GetMenuItemCount(Some(menu)) };
for i in 0..n.max(0) as u32 {
let (label, accel) = split_caption(&item_text(menu, i));
// SAFETY: as above.
let sub = unsafe { GetSubMenu(menu, i as i32) };
if !sub.is_invalid() {
if depth < MAX_DEPTH {
path.push(label);
walk(sub, depth + 1, path, out);
path.pop();
}
continue;
}
if label.is_empty() {
continue; // separator
}
// SAFETY: as above.
let id = unsafe { GetMenuItemID(menu, i as i32) };
if id == 0 || id == u32::MAX {
continue;
}
let mut full = path.clone();
full.push(label);
out.push(MenuItem { path: full, id, accel });
}
}
/// Every leaf command in the window's menu bar, top level first in each path. Empty when
/// the window has no Win32 menu (an owner-drawn or wx-custom bar, or not a frame).
pub fn tree(h: HWND) -> Result<Vec<MenuItem>, String> {
// SAFETY: GetMenu reads the window's menu handle; no message is sent.
let bar = unsafe { GetMenu(h) };
if bar.is_invalid() {
return Err(format!("window {} has no Win32 menu bar (wrong hwnd, a dialog, or a custom-drawn bar)", hwnd_u64(h)));
}
let mut out = Vec::with_capacity(128);
walk(bar, 0, &mut Vec::new(), &mut out);
Ok(out)
}
/// PostMessageW WM_COMMAND with the menu id: the action runs on KiCad's thread with no
/// focus, no cursor and no wait.
pub fn invoke(h: HWND, id: u32) -> Result<(), String> {
if id == 0 {
return Err("menu id 0 is not a command".into());
}
post(h, WM_COMMAND, id as usize, 0)
}
#[cfg(test)]
mod tests {
use super::split_caption;
#[test]
fn captions_split_into_label_and_accelerator() {
assert_eq!(split_caption("&Save\tCtrl+S"), ("Save".into(), Some("Ctrl+S".into())));
assert_eq!(split_caption("3D &Viewer\tAlt+3"), ("3D Viewer".into(), Some("Alt+3".into())));
assert_eq!(split_caption("E&xit"), ("Exit".into(), None));
assert_eq!(split_caption(""), (String::new(), None));
}
}