master
John Lauer rust: Windows window-control layer (enumerate, process, messages, menu, focus, capture, UIA), all cross-process calls h… 3ecdded 2mo ago
//! 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));
    }
}