From e33c5f0daf7b2a1266219bc2cb6d6a73df41c9b0 Mon Sep 17 00:00:00 2001 From: "Ngo Iok Ui (Wu Yu Wei)" Date: Wed, 20 Jul 2022 20:34:09 +0800 Subject: [PATCH] feat: add accelerator module (#17) * Add accelerator module * Add Linux port * Add macOS port * Add Windows port * Remove unused types * Fix doc tests * Add more variants --- Cargo.toml | 2 + examples/tao.rs | 12 +- examples/winit.rs | 12 +- src/accelerator.rs | 288 +++++++++++++++++++ src/lib.rs | 36 +-- src/platform_impl/linux/accelerator.rs | 194 +++++++++++-- src/platform_impl/linux/mod.rs | 46 +-- src/platform_impl/macos/accelerator.rs | 217 ++++++++------- src/platform_impl/macos/menu_item.rs | 18 +- src/platform_impl/macos/mod.rs | 76 +++-- src/platform_impl/windows/accelerator.rs | 340 ++++++++++++++++------- src/platform_impl/windows/mod.rs | 16 +- 12 files changed, 931 insertions(+), 326 deletions(-) create mode 100644 src/accelerator.rs diff --git a/Cargo.toml b/Cargo.toml index 11c70c3..2898738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ categories = ["gui"] [dependencies] crossbeam-channel = "0.5" once_cell = "1.10" +keyboard-types = "0.6" [target.'cfg(target_os = "windows")'.dependencies.windows-sys] version = "0.34" @@ -26,6 +27,7 @@ features = [ ] [target.'cfg(target_os = "linux")'.dependencies] +gdk = "0.15" gtk = "0.15" libxdo = "0.6.0" diff --git a/examples/tao.rs b/examples/tao.rs index d46a8b6..c0840e9 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -1,4 +1,8 @@ -use muda::{menu_event_receiver, Menu, NativeMenuItem}; +use keyboard_types::Code; +use muda::{ + accelerator::{Accelerator, Mods}, + menu_event_receiver, Menu, NativeMenuItem, +}; #[cfg(target_os = "linux")] use tao::platform::unix::WindowExtUnix; #[cfg(target_os = "windows")] @@ -19,7 +23,11 @@ fn main() { let mut file_menu = menu_bar.add_submenu("&File", true); let mut open_item = file_menu.add_item("&Open", true, None); - let mut save_item = file_menu.add_item("&Save", true, Some("CommandOrCtrl+S")); + let mut save_item = file_menu.add_item( + "&Save", + true, + Some(Accelerator::new(Mods::Ctrl, Code::KeyS)), + ); file_menu.add_native_item(NativeMenuItem::Minimize); file_menu.add_native_item(NativeMenuItem::CloseWindow); file_menu.add_native_item(NativeMenuItem::Quit); diff --git a/examples/winit.rs b/examples/winit.rs index bce8d42..24ef5c9 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,4 +1,8 @@ -use muda::{menu_event_receiver, Menu, NativeMenuItem}; +use keyboard_types::Code; +use muda::{ + accelerator::{Accelerator, Mods}, + menu_event_receiver, Menu, NativeMenuItem, +}; #[cfg(target_os = "macos")] use winit::platform::macos::EventLoopBuilderExtMacOS; #[cfg(target_os = "windows")] @@ -37,7 +41,11 @@ fn main() { let mut file_menu = menu_bar.add_submenu("&File", true); let mut open_item = file_menu.add_item("&Open", true, None); - let mut save_item = file_menu.add_item("&Save", true, Some("CommandOrCtrl+S")); + let mut save_item = file_menu.add_item( + "&Save", + true, + Some(Accelerator::new(Mods::Ctrl, Code::KeyS)), + ); file_menu.add_native_item(NativeMenuItem::Minimize); file_menu.add_native_item(NativeMenuItem::CloseWindow); file_menu.add_native_item(NativeMenuItem::Quit); diff --git a/src/accelerator.rs b/src/accelerator.rs new file mode 100644 index 0000000..034a1c9 --- /dev/null +++ b/src/accelerator.rs @@ -0,0 +1,288 @@ +//! Accelerators describe keyboard shortcuts defined by the application. +//! +//! [`Accelerator`s](crate::accelerator::Accelerator) are used to define a keyboard shortcut consisting +//! of an optional combination of modifier keys (provided by [`SysMods`](crate::accelerator::SysMods), +//! [`RawMods`](crate::accelerator::RawMods) or [`Modifiers`](crate::accelerator::Modifiers)) and +//! one key ([`Code`](crate::accelerator::Code)). +//! +//! # Examples +//! They can be created directly +//! ``` +//! # use muda::accelerator::{Accelerator, Mods, Modifiers, Code}; +//! # +//! let accelerator = Accelerator::new(Mods::Shift, Code::KeyQ); +//! let accelerator_with_raw_mods = Accelerator::new(Mods::Shift, Code::KeyQ); +//! let accelerator_without_mods = Accelerator::new(None, Code::KeyQ); +//! # assert_eq!(accelerator, accelerator_with_raw_mods); +//! ``` +//! or from `&str`, note that all modifiers +//! have to be listed before the non-modifier key, `shift+alt+KeyQ` is legal, +//! whereas `shift+q+alt` is not. +//! ``` +//! # use muda::accelerator::{Accelerator, Mods}; +//! # +//! let accelerator: Accelerator = "shift+alt+KeyQ".parse().unwrap(); +//! # +//! # // This assert exists to ensure a test breaks once the +//! # // statement above about ordering is no longer valid. +//! # assert!("shift+KeyQ+alt".parse::().is_err()); +//! ``` +//! + +pub use keyboard_types::{Code, Modifiers}; +use std::{borrow::Borrow, hash::Hash, str::FromStr}; + +/// Base `Accelerator` functions. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct Accelerator { + pub(crate) mods: Modifiers, + pub(crate) key: Code, +} + +impl Accelerator { + /// Creates a new accelerator to define keyboard shortcuts throughout your application. + pub fn new(mods: impl Into>, key: Code) -> Self { + Self { + mods: mods.into().unwrap_or_else(Modifiers::empty), + key, + } + } + + /// Returns `true` if this [`Code`] and [`Modifiers`] matches this `Accelerator`. + /// + /// [`Code`]: Code + /// [`Modifiers`]: crate::accelerator::Modifiers + pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { + // Should be a const but const bit_or doesn't work here. + let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; + let modifiers = modifiers.borrow(); + let key = key.borrow(); + self.mods == *modifiers & base_mods && self.key == *key + } +} + +// Accelerator::from_str is available to be backward +// compatible with tauri and it also open the option +// to generate accelerator from string +impl FromStr for Accelerator { + type Err = AcceleratorParseError; + fn from_str(accelerator_string: &str) -> Result { + parse_accelerator(accelerator_string) + } +} + +/// Represents the active modifier keys. +/// +/// This is intended to be clearer than [`Modifiers`], when describing accelerators. +/// +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Hash)] +pub enum Mods { + None, + Alt, + Ctrl, + Command, + CommandOrCtrl, + Meta, + Shift, + AltCtrl, + AltMeta, + AltShift, + CtrlShift, + CtrlMeta, + MetaShift, + AltCtrlMeta, + AltCtrlShift, + AltMetaShift, + CtrlMetaShift, + AltCtrlMetaShift, +} + +impl From for Option { + fn from(src: Mods) -> Option { + Some(src.into()) + } +} + +impl From for Modifiers { + fn from(src: Mods) -> Modifiers { + let (alt, ctrl, meta, shift) = match src { + Mods::None => (false, false, false, false), + Mods::Alt => (true, false, false, false), + Mods::Ctrl => (false, true, false, false), + Mods::Command => (false, false, true, false), + #[cfg(target_os = "macos")] + Mods::CommandOrCtrl => (false, false, true, false), + #[cfg(not(target_os = "macos"))] + Mods::CommandOrCtrl => (false, true, false, false), + Mods::Meta => (false, false, true, false), + Mods::Shift => (false, false, false, true), + Mods::AltCtrl => (true, true, false, false), + Mods::AltMeta => (true, false, true, false), + Mods::AltShift => (true, false, false, true), + Mods::CtrlMeta => (false, true, true, false), + Mods::CtrlShift => (false, true, false, true), + Mods::MetaShift => (false, false, true, true), + Mods::AltCtrlMeta => (true, true, true, false), + Mods::AltMetaShift => (true, false, true, true), + Mods::AltCtrlShift => (true, true, false, true), + Mods::CtrlMetaShift => (false, true, true, true), + Mods::AltCtrlMetaShift => (true, true, true, true), + }; + let mut mods = Modifiers::empty(); + mods.set(Modifiers::ALT, alt); + mods.set(Modifiers::CONTROL, ctrl); + mods.set(Modifiers::SUPER, meta); + mods.set(Modifiers::SHIFT, shift); + mods + } +} + +#[derive(Debug, Clone)] +pub struct AcceleratorParseError(String); + +impl std::fmt::Display for AcceleratorParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[AcceleratorParseError]: {}", self.0) + } +} + +fn parse_accelerator(accelerator_string: &str) -> Result { + let mut mods = Modifiers::empty(); + let mut key = Code::Unidentified; + + for raw in accelerator_string.split('+') { + let token = raw.trim().to_string(); + if token.is_empty() { + return Err(AcceleratorParseError( + "Unexpected empty token while parsing accelerator".into(), + )); + } + + if key != Code::Unidentified { + // at this point we already parsed the modifiers and found a main key but + // the function received more then one main key or it is not in the right order + // examples: + // 1. "Ctrl+Shift+C+A" => only one main key should be allowd. + // 2. "Ctrl+C+Shift" => wrong order + return Err(AcceleratorParseError(format!( + "Unexpected accelerator string format: \"{}\"", + accelerator_string + ))); + } + + match token.to_uppercase().as_str() { + "OPTION" | "ALT" => { + mods.set(Modifiers::ALT, true); + } + "CONTROL" | "CTRL" => { + mods.set(Modifiers::CONTROL, true); + } + "COMMAND" | "CMD" | "SUPER" => { + mods.set(Modifiers::SUPER, true); + } + "SHIFT" => { + mods.set(Modifiers::SHIFT, true); + } + "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { + #[cfg(target_os = "macos")] + mods.set(Modifiers::SUPER, true); + #[cfg(not(target_os = "macos"))] + mods.set(Modifiers::CONTROL, true); + } + _ => { + if let Ok(code) = Code::from_str(token.as_str()) { + match code { + Code::Unidentified => { + return Err(AcceleratorParseError(format!( + "Couldn't identify \"{}\" as a valid `Code`", + token + ))) + } + _ => key = code, + } + } else { + return Err(AcceleratorParseError(format!( + "Couldn't identify \"{}\" as a valid `Code`", + token + ))); + } + } + } + } + + Ok(Accelerator { key, mods }) +} + +#[test] +fn test_parse_accelerator() { + assert_eq!( + parse_accelerator("CTRL+KeyX").unwrap(), + Accelerator { + mods: Modifiers::CONTROL, + key: Code::KeyX, + } + ); + assert_eq!( + parse_accelerator("SHIFT+KeyC").unwrap(), + Accelerator { + mods: Modifiers::SHIFT, + key: Code::KeyC, + } + ); + assert_eq!( + parse_accelerator("CTRL+KeyZ").unwrap(), + Accelerator { + mods: Modifiers::CONTROL, + key: Code::KeyZ, + } + ); + assert_eq!( + parse_accelerator("super+ctrl+SHIFT+alt+ArrowUp").unwrap(), + Accelerator { + mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT, + key: Code::ArrowUp, + } + ); + assert_eq!( + parse_accelerator("Digit5").unwrap(), + Accelerator { + mods: Modifiers::empty(), + key: Code::Digit5, + } + ); + assert_eq!( + parse_accelerator("KeyG").unwrap(), + Accelerator { + mods: Modifiers::empty(), + key: Code::KeyG, + } + ); + + let acc = parse_accelerator("+G"); + assert!(acc.is_err()); + + let acc = parse_accelerator("SHGSH+G"); + assert!(acc.is_err()); + + assert_eq!( + parse_accelerator("SHiFT+F12").unwrap(), + Accelerator { + mods: Modifiers::SHIFT, + key: Code::F12, + } + ); + assert_eq!( + parse_accelerator("CmdOrCtrl+Space").unwrap(), + Accelerator { + #[cfg(target_os = "macos")] + mods: Modifiers::SUPER, + #[cfg(not(target_os = "macos"))] + mods: Modifiers::CONTROL, + key: Code::Space, + } + ); + + let acc = parse_accelerator("CTRL+"); + assert!(acc.is_err()); +} diff --git a/src/lib.rs b/src/lib.rs index cd86a92..3c2c9d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,14 +3,14 @@ //! //! Before you can add submenus and menu items, you first need a root or a base menu. //! ```no_run -//! let mut menu = Menu::new(); +//! let mut menu = muda::Menu::new(); //! ``` //! //! # Adding submens to the root menu //! //! Once you have a root menu you can start adding [`Submenu`]s by using [`Menu::add_submenu`]. //! ```no_run -//! let mut menu = Menu::new(); +//! let mut menu = muda::Menu::new(); //! let file_menu = menu.add_submenu("File", true); //! let edit_menu = menu.add_submenu("Edit", true); //! ``` @@ -19,22 +19,22 @@ //! //! Once you have a [`Submenu`] you can star creating more [`Submenu`]s or [`MenuItem`]s. //! ```no_run -//! let mut menu = Menu::new(); +//! let mut menu = muda::Menu::new(); //! -//! let file_menu = menu.add_submenu("File", true); -//! let open_item = file_menu.add_text_item("Open", true); -//! let save_item = file_menu.add_text_item("Save", true); +//! let mut file_menu = menu.add_submenu("File", true); +//! let open_item = file_menu.add_item("Open", true, None); +//! let save_item = file_menu.add_item("Save", true, None); //! -//! let edit_menu = menu.add_submenu("Edit", true); -//! let copy_item = file_menu.add_text_item("Copy", true); -//! let cut_item = file_menu.add_text_item("Cut", true); +//! let mut edit_menu = menu.add_submenu("Edit", true); +//! let copy_item = file_menu.add_item("Copy", true, None); +//! let cut_item = file_menu.add_item("Cut", true, None); //! ``` //! //! # Add your root menu to a Window (Windows and Linux Only) //! //! You can use [`Menu`] to display a top menu in a Window on Windows and Linux. -//! ```no_run -//! let mut menu = Menu::new(); +//! ```ignore +//! let mut menu = muda::Menu::new(); //! // --snip-- //! #[cfg(target_os = "windows")] //! menu.init_for_hwnd(window.hwnd() as isize); @@ -48,8 +48,8 @@ //! //! You can use [`menu_event_receiver`] to get a reference to the [`MenuEventReceiver`] //! which you can use to listen to events when a menu item is activated -//! ```no_run -//! if let Ok(event) = menu_event_receiver().try_recv() { +//! ```ignore +//! if let Ok(event) = muda::menu_event_receiver().try_recv() { //! match event.id { //! _ if event.id == save_item.id() => { //! println!("Save menu item activated"); @@ -59,9 +59,11 @@ //! } //! ``` +use accelerator::Accelerator; use crossbeam_channel::{unbounded, Receiver, Sender}; use once_cell::sync::Lazy; +pub mod accelerator; mod counter; mod platform_impl; @@ -85,8 +87,8 @@ pub struct MenuEvent { /// /// # Example /// -/// ``` -/// let mut menu = Menu::new(); +/// ```no_run +/// let mut menu = muda::Menu::new(); /// let file_menu = menu.add_submenu("File", true); /// let edit_menu = menu.add_submenu("Edit", true); /// ``` @@ -278,7 +280,7 @@ impl Submenu { &mut self, label: S, enabled: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> MenuItem { MenuItem(self.0.add_item(label, enabled, accelerator)) } @@ -294,7 +296,7 @@ impl Submenu { label: S, enabled: bool, checked: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> CheckMenuItem { CheckMenuItem(self.0.add_check_item(label, enabled, checked, accelerator)) } diff --git a/src/platform_impl/linux/accelerator.rs b/src/platform_impl/linux/accelerator.rs index b8898bf..b2e4449 100644 --- a/src/platform_impl/linux/accelerator.rs +++ b/src/platform_impl/linux/accelerator.rs @@ -1,3 +1,8 @@ +use gtk::{prelude::*, AccelGroup}; +use keyboard_types::{Code, Modifiers}; + +use crate::accelerator::Accelerator; + pub fn to_gtk_menemenoic>(string: S) -> String { string .as_ref() @@ -6,39 +11,168 @@ pub fn to_gtk_menemenoic>(string: S) -> String { .replace("[~~]", "&&") } -pub fn to_gtk_accelerator>(accelerator: S) -> String { - let accelerator = accelerator.as_ref(); - let mut s = accelerator.split("+"); - let count = s.clone().count(); - let (mod1, mod2, key) = { - if count == 2 { - (s.next().unwrap(), None, s.next().unwrap()) - } else if count == 3 { - ( - s.next().unwrap(), - Some(s.next().unwrap()), - s.next().unwrap(), - ) - } else { - panic!("Unsupported accelerator format: {}", accelerator) +pub fn register_accelerator>( + item: &M, + accel_group: &AccelGroup, + menu_key: &Accelerator, +) { + let accel_key = match &menu_key.key { + Code::KeyA => 'A' as u32, + Code::KeyB => 'B' as u32, + Code::KeyC => 'C' as u32, + Code::KeyD => 'D' as u32, + Code::KeyE => 'E' as u32, + Code::KeyF => 'F' as u32, + Code::KeyG => 'G' as u32, + Code::KeyH => 'H' as u32, + Code::KeyI => 'I' as u32, + Code::KeyJ => 'J' as u32, + Code::KeyK => 'K' as u32, + Code::KeyL => 'L' as u32, + Code::KeyM => 'M' as u32, + Code::KeyN => 'N' as u32, + Code::KeyO => 'O' as u32, + Code::KeyP => 'P' as u32, + Code::KeyQ => 'Q' as u32, + Code::KeyR => 'R' as u32, + Code::KeyS => 'S' as u32, + Code::KeyT => 'T' as u32, + Code::KeyU => 'U' as u32, + Code::KeyV => 'V' as u32, + Code::KeyW => 'W' as u32, + Code::KeyX => 'X' as u32, + Code::KeyY => 'Y' as u32, + Code::KeyZ => 'Z' as u32, + Code::Digit0 => '0' as u32, + Code::Digit1 => '1' as u32, + Code::Digit2 => '2' as u32, + Code::Digit3 => '3' as u32, + Code::Digit4 => '4' as u32, + Code::Digit5 => '5' as u32, + Code::Digit6 => '6' as u32, + Code::Digit7 => '7' as u32, + Code::Digit8 => '8' as u32, + Code::Digit9 => '9' as u32, + Code::Comma => ',' as u32, + Code::Minus => '-' as u32, + Code::Period => '.' as u32, + Code::Space => ' ' as u32, + Code::Equal => '=' as u32, + Code::Semicolon => ';' as u32, + Code::Slash => '/' as u32, + Code::Backslash => '\\' as u32, + Code::Quote => '\'' as u32, + Code::Backquote => '`' as u32, + Code::BracketLeft => '[' as u32, + Code::BracketRight => ']' as u32, + k => { + if let Some(gdk_key) = key_to_raw_key(k) { + *gdk_key + } else { + dbg!("Cannot map key {:?}", k); + return; + } } }; - let mut gtk_accelerator = parse_mod(mod1).to_string(); - if let Some(mod2) = mod2 { - gtk_accelerator.push_str(parse_mod(mod2)); - } - gtk_accelerator.push_str(key); - - gtk_accelerator + item.add_accelerator( + "activate", + accel_group, + accel_key, + modifiers_to_gdk_modifier_type(menu_key.mods), + gtk::AccelFlags::VISIBLE, + ); } -fn parse_mod(modifier: &str) -> &str { - match modifier.to_uppercase().as_str() { - "SHIFT" => "", - "CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => "", - "ALT" => "", - "SUPER" | "META" | "WIN" => "", - _ => panic!("Unsupported modifier: {}", modifier), - } +fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType { + let mut result = gdk::ModifierType::empty(); + + result.set( + gdk::ModifierType::MOD1_MASK, + modifiers.contains(Modifiers::ALT), + ); + result.set( + gdk::ModifierType::CONTROL_MASK, + modifiers.contains(Modifiers::CONTROL), + ); + result.set( + gdk::ModifierType::SHIFT_MASK, + modifiers.contains(Modifiers::SHIFT), + ); + result.set( + gdk::ModifierType::META_MASK, + modifiers.contains(Modifiers::SUPER), + ); + + result +} + +fn key_to_raw_key(src: &Code) -> Option { + use gdk::keys::constants::*; + Some(match src { + Code::Escape => Escape, + Code::Backspace => BackSpace, + + Code::Tab => Tab, + Code::Enter => Return, + + Code::ControlLeft => Control_L, + Code::AltLeft => Alt_L, + Code::ShiftLeft => Shift_L, + Code::MetaLeft => Super_L, + + Code::ControlRight => Control_R, + Code::AltRight => Alt_R, + Code::ShiftRight => Shift_R, + Code::MetaRight => Super_R, + + Code::CapsLock => Caps_Lock, + Code::F1 => F1, + Code::F2 => F2, + Code::F3 => F3, + Code::F4 => F4, + Code::F5 => F5, + Code::F6 => F6, + Code::F7 => F7, + Code::F8 => F8, + Code::F9 => F9, + Code::F10 => F10, + Code::F11 => F11, + Code::F12 => F12, + Code::F13 => F13, + Code::F14 => F14, + Code::F15 => F15, + Code::F16 => F16, + Code::F17 => F17, + Code::F18 => F18, + Code::F19 => F19, + Code::F20 => F20, + Code::F21 => F21, + Code::F22 => F22, + Code::F23 => F23, + Code::F24 => F24, + + Code::PrintScreen => Print, + Code::ScrollLock => Scroll_Lock, + // Pause/Break not audio. + Code::Pause => Pause, + + Code::Insert => Insert, + Code::Delete => Delete, + Code::Home => Home, + Code::End => End, + Code::PageUp => Page_Up, + Code::PageDown => Page_Down, + + Code::NumLock => Num_Lock, + + Code::ArrowUp => Up, + Code::ArrowDown => Down, + Code::ArrowLeft => Left, + Code::ArrowRight => Right, + + Code::ContextMenu => Menu, + Code::WakeUp => WakeUp, + _ => return None, + }) } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index b62777b..309a1ee 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -1,7 +1,7 @@ mod accelerator; -use crate::{counter::Counter, NativeMenuItem}; -use accelerator::{to_gtk_accelerator, to_gtk_menemenoic}; +use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem}; +use accelerator::{register_accelerator, to_gtk_menemenoic}; use gtk::{prelude::*, Orientation}; use std::{cell::RefCell, collections::HashMap, rc::Rc}; @@ -14,7 +14,7 @@ struct MenuEntry { enabled: bool, checked: bool, id: u64, - accelerator: Option, + accelerator: Option, r#type: MenuEntryType, entries: Option>>>, } @@ -247,7 +247,7 @@ impl Submenu { &mut self, label: S, enabled: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> MenuItem { let label = label.as_ref().to_string(); let id = COUNTER.next(); @@ -257,7 +257,7 @@ impl Submenu { enabled, r#type: MenuEntryType::MenuItem(Vec::new()), id, - accelerator: accelerator.map(|s| s.to_string()), + accelerator: accelerator.clone(), ..Default::default() })); @@ -265,13 +265,7 @@ impl Submenu { if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type { for (_, menu) in native_menus { - let item = create_gtk_menu_item( - &label, - enabled, - &accelerator.map(|s| s.to_string()), - id, - &*self.1, - ); + let item = create_gtk_menu_item(&label, enabled, &accelerator, id, &*self.1); menu.append(&item); if let MenuEntryType::MenuItem(native_items) = &mut entry.borrow_mut().r#type { native_items.push(item); @@ -304,7 +298,7 @@ impl Submenu { label: S, enabled: bool, checked: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> CheckMenuItem { let label = label.as_ref().to_string(); let id = COUNTER.next(); @@ -315,7 +309,7 @@ impl Submenu { checked, r#type: MenuEntryType::CheckMenuItem(Vec::new()), id, - accelerator: accelerator.map(|s| s.to_string()), + accelerator: accelerator.clone(), ..Default::default() })); @@ -327,7 +321,7 @@ impl Submenu { &label, enabled, checked, - &accelerator.map(|s| s.to_string()), + &accelerator, id, &*self.1, ); @@ -511,21 +505,14 @@ fn create_gtk_submenu(label: &str, enabled: bool) -> (gtk::MenuItem, gtk::Menu) fn create_gtk_menu_item( label: &str, enabled: bool, - accelerator: &Option, + accelerator: &Option, id: u64, accel_group: >k::AccelGroup, ) -> gtk::MenuItem { let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label)); item.set_sensitive(enabled); if let Some(accelerator) = accelerator { - let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); - item.add_accelerator( - "activate", - accel_group, - key, - modifiers, - gtk::AccelFlags::VISIBLE, - ); + register_accelerator(&item, accel_group, accelerator); } item.connect_activate(move |_| { let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); @@ -538,7 +525,7 @@ fn create_gtk_check_menu_item( label: &str, enabled: bool, checked: bool, - accelerator: &Option, + accelerator: &Option, id: u64, accel_group: >k::AccelGroup, ) -> gtk::CheckMenuItem { @@ -546,14 +533,7 @@ fn create_gtk_check_menu_item( item.set_sensitive(enabled); item.set_active(checked); if let Some(accelerator) = accelerator { - let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); - item.add_accelerator( - "activate", - accel_group, - key, - modifiers, - gtk::AccelFlags::VISIBLE, - ); + register_accelerator(&item, accel_group, accelerator); } item.connect_activate(move |_| { let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); diff --git a/src/platform_impl/macos/accelerator.rs b/src/platform_impl/macos/accelerator.rs index 2850815..c45e829 100644 --- a/src/platform_impl/macos/accelerator.rs +++ b/src/platform_impl/macos/accelerator.rs @@ -1,108 +1,133 @@ use cocoa::appkit::NSEventModifierFlags; +use keyboard_types::{Code, Modifiers}; + +use crate::accelerator::Accelerator; /// Mnemonic is deprecated since macOS 10 pub fn remove_mnemonic(string: impl AsRef) -> String { string.as_ref().replace("&", "") } -/// Returns a tuple of (Key, Modifier) -pub fn parse_accelerator(accelerator: impl AsRef) -> (String, NSEventModifierFlags) { - let accelerator = accelerator.as_ref(); - let mut s = accelerator.split("+"); - let count = s.clone().count(); - let (mod1, mod2, key) = { - if count == 2 { - (s.next().unwrap(), None, s.next().unwrap()) - } else if count == 3 { - ( - s.next().unwrap(), - Some(s.next().unwrap()), - s.next().unwrap(), - ) - } else { - panic!("Unsupported accelerator format: {}", accelerator) +impl Accelerator { + /// Return the string value of this hotkey, for use with Cocoa `NSResponder` + /// objects. + /// + /// Returns the empty string if no key equivalent is known. + pub fn key_equivalent(&self) -> String { + match self.key { + Code::KeyA => "a".into(), + Code::KeyB => "b".into(), + Code::KeyC => "c".into(), + Code::KeyD => "d".into(), + Code::KeyE => "e".into(), + Code::KeyF => "f".into(), + Code::KeyG => "g".into(), + Code::KeyH => "h".into(), + Code::KeyI => "i".into(), + Code::KeyJ => "j".into(), + Code::KeyK => "k".into(), + Code::KeyL => "l".into(), + Code::KeyM => "m".into(), + Code::KeyN => "n".into(), + Code::KeyO => "o".into(), + Code::KeyP => "p".into(), + Code::KeyQ => "q".into(), + Code::KeyR => "r".into(), + Code::KeyS => "s".into(), + Code::KeyT => "t".into(), + Code::KeyU => "u".into(), + Code::KeyV => "v".into(), + Code::KeyW => "w".into(), + Code::KeyX => "x".into(), + Code::KeyY => "y".into(), + Code::KeyZ => "z".into(), + Code::Digit0 => "0".into(), + Code::Digit1 => "1".into(), + Code::Digit2 => "2".into(), + Code::Digit3 => "3".into(), + Code::Digit4 => "4".into(), + Code::Digit5 => "5".into(), + Code::Digit6 => "6".into(), + Code::Digit7 => "7".into(), + Code::Digit8 => "8".into(), + Code::Digit9 => "9".into(), + Code::Comma => ",".into(), + Code::Minus => "-".into(), + Code::Period => ".".into(), + Code::Space => "\u{0020}".into(), + Code::Equal => "=".into(), + Code::Semicolon => ";".into(), + Code::Slash => "/".into(), + Code::Backslash => "\\".into(), + Code::Quote => "\'".into(), + Code::Backquote => "`".into(), + Code::BracketLeft => "[".into(), + Code::BracketRight => "]".into(), + Code::Tab => "⇥".into(), + Code::Escape => "\u{001b}".into(), + // from NSText.h + Code::Enter => "\u{0003}".into(), + Code::Backspace => "\u{0008}".into(), + Code::Delete => "\u{007f}".into(), + // from NSEvent.h + Code::Insert => "\u{F727}".into(), + Code::Home => "\u{F729}".into(), + Code::End => "\u{F72B}".into(), + Code::PageUp => "\u{F72C}".into(), + Code::PageDown => "\u{F72D}".into(), + Code::PrintScreen => "\u{F72E}".into(), + Code::ScrollLock => "\u{F72F}".into(), + Code::ArrowUp => "\u{F700}".into(), + Code::ArrowDown => "\u{F701}".into(), + Code::ArrowLeft => "\u{F702}".into(), + Code::ArrowRight => "\u{F703}".into(), + Code::F1 => "\u{F704}".into(), + Code::F2 => "\u{F705}".into(), + Code::F3 => "\u{F706}".into(), + Code::F4 => "\u{F707}".into(), + Code::F5 => "\u{F708}".into(), + Code::F6 => "\u{F709}".into(), + Code::F7 => "\u{F70A}".into(), + Code::F8 => "\u{F70B}".into(), + Code::F9 => "\u{F70C}".into(), + Code::F10 => "\u{F70D}".into(), + Code::F11 => "\u{F70E}".into(), + Code::F12 => "\u{F70F}".into(), + Code::F13 => "\u{F710}".into(), + Code::F14 => "\u{F711}".into(), + Code::F15 => "\u{F712}".into(), + Code::F16 => "\u{F713}".into(), + Code::F17 => "\u{F714}".into(), + Code::F18 => "\u{F715}".into(), + Code::F19 => "\u{F716}".into(), + Code::F20 => "\u{F717}".into(), + Code::F21 => "\u{F718}".into(), + Code::F22 => "\u{F719}".into(), + Code::F23 => "\u{F71A}".into(), + Code::F24 => "\u{F71B}".into(), + _ => { + #[cfg(debug_assertions)] + eprintln!("no key equivalent for {:?}", self); + "".into() + } } - }; - - let mut mods = NSEventModifierFlags::empty(); - let mod1_flag = parse_mod(mod1); - mods |= mod1_flag; - if let Some(mod2) = mod2 { - let mod2_flag = parse_mod(mod2); - mods |= mod2_flag; } - let key_equivalent = parse_key(key); - - (key_equivalent, mods) -} - -fn parse_mod(modifier: &str) -> NSEventModifierFlags { - match modifier.to_uppercase().as_str() { - "SHIFT" => NSEventModifierFlags::NSShiftKeyMask, - "CONTROL" | "CTRL" => NSEventModifierFlags::NSControlKeyMask, - "OPTION" | "ALT" => NSEventModifierFlags::NSAlternateKeyMask, - "COMMAND" | "CMD" | "SUPER" | "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" - | "CMDORCONTROL" => NSEventModifierFlags::NSCommandKeyMask, - _ => panic!("Unsupported modifier: {}", modifier), - } -} - -fn parse_key(key: &str) -> String { - match key.to_uppercase().as_str() { - "SPACE" => "\u{0020}".into(), - "BACKSPACE" => "\u{0008}".into(), - "TAB" => "⇥".into(), - "ENTER" | "RETURN" => "\u{0003}".into(), - "ESC" | "ESCAPE" => "\u{001b}".into(), - "PAGEUP" => "\u{F72C}".into(), - "PAGEDOWN" => "\u{F72D}".into(), - "END" => "\u{F72B}".into(), - "HOME" => "\u{F729}".into(), - "LEFTARROW" => "\u{F702}".into(), - "UPARROW" => "\u{F700}".into(), - "RIGHTARROW" => "\u{F703}".into(), - "DOWNARROW" => "\u{F701}".into(), - "DELETE" => "\u{007f}".into(), - "0" => "0".into(), - "1" => "1".into(), - "2" => "2".into(), - "3" => "3".into(), - "4" => "4".into(), - "5" => "5".into(), - "6" => "6".into(), - "7" => "7".into(), - "8" => "8".into(), - "9" => "9".into(), - "A" => "a".into(), - "B" => "b".into(), - "C" => "c".into(), - "D" => "d".into(), - "E" => "e".into(), - "F" => "f".into(), - "G" => "g".into(), - "H" => "h".into(), - "I" => "i".into(), - "J" => "j".into(), - "K" => "k".into(), - "L" => "l".into(), - "M" => "m".into(), - "N" => "n".into(), - "O" => "o".into(), - "P" => "p".into(), - "Q" => "q".into(), - "R" => "r".into(), - "S" => "s".into(), - "T" => "t".into(), - "U" => "u".into(), - "V" => "v".into(), - "W" => "w".into(), - "X" => "x".into(), - "Y" => "y".into(), - "Z" => "z".into(), - "," => ",".into(), - "." => ".".into(), - "/" => "/".into(), - _ => panic!("Unsupported modifier: {}", key), + pub fn key_modifier_mask(&self) -> NSEventModifierFlags { + let mods: Modifiers = self.mods; + let mut flags = NSEventModifierFlags::empty(); + if mods.contains(Modifiers::SHIFT) { + flags.insert(NSEventModifierFlags::NSShiftKeyMask); + } + if mods.contains(Modifiers::SUPER) { + flags.insert(NSEventModifierFlags::NSCommandKeyMask); + } + if mods.contains(Modifiers::ALT) { + flags.insert(NSEventModifierFlags::NSAlternateKeyMask); + } + if mods.contains(Modifiers::CONTROL) { + flags.insert(NSEventModifierFlags::NSControlKeyMask); + } + flags } } diff --git a/src/platform_impl/macos/menu_item.rs b/src/platform_impl/macos/menu_item.rs index 3aee1d1..6988d82 100644 --- a/src/platform_impl/macos/menu_item.rs +++ b/src/platform_impl/macos/menu_item.rs @@ -1,5 +1,6 @@ +use crate::accelerator::Accelerator; use crate::counter::Counter; -use crate::platform_impl::platform_impl::accelerator::{parse_accelerator, remove_mnemonic}; +use crate::platform_impl::platform_impl::accelerator::remove_mnemonic; use cocoa::{ appkit::{NSButton, NSEventModifierFlags, NSMenuItem}, base::{id, nil, BOOL, NO, YES}, @@ -29,7 +30,7 @@ impl MenuItem { label: S, enabled: bool, selector: Sel, - accelerator: Option<&str>, + accelerator: Option, ) -> Self { let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); @@ -95,7 +96,7 @@ impl CheckMenuItem { enabled: bool, checked: bool, selector: Sel, - accelerator: Option<&str>, + accelerator: Option, ) -> Self { let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); @@ -169,7 +170,11 @@ impl CheckMenuItem { } } -pub fn make_menu_item(title: &str, selector: Sel, accelerator: Option<&str>) -> (u64, *mut Object) { +pub fn make_menu_item( + title: &str, + selector: Sel, + accelerator: Option, +) -> (u64, *mut Object) { let alloc = make_menu_item_alloc(); let menu_id = COUNTER.next(); @@ -221,12 +226,13 @@ fn make_menu_item_from_alloc( alloc: *mut Object, title: *mut Object, selector: Sel, - accelerator: Option<&str>, + accelerator: Option, ) -> *mut Object { unsafe { let (key_equivalent, masks) = match accelerator { Some(accelerator) => { - let (key, mods) = parse_accelerator(accelerator); + let key = accelerator.key_equivalent(); + let mods = accelerator.key_modifier_mask(); let key = NSString::alloc(nil).init_str(&key); (key, mods) } diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 0c314bf..000282b 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,13 +1,15 @@ mod accelerator; mod menu_item; -use crate::platform_impl::platform_impl::menu_item::make_menu_item; +use crate::accelerator::{RawMods, SysMods}; use crate::NativeMenuItem; +use crate::{accelerator::Accelerator, platform_impl::platform_impl::menu_item::make_menu_item}; use cocoa::{ appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, base::{id, nil, selector, NO}, foundation::{NSAutoreleasePool, NSString}, }; +use keyboard_types::Code; use objc::{class, msg_send, sel, sel_impl}; use self::accelerator::remove_mnemonic; @@ -90,7 +92,7 @@ impl Submenu { &mut self, label: S, enabled: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> MenuItem { let item = MenuItem::new(label, enabled, sel!(fireMenubarAction:), accelerator); unsafe { @@ -110,17 +112,25 @@ impl Submenu { None, ) } - NativeMenuItem::CloseWindow => { - make_menu_item("Close Window", selector("performClose:"), Some("Command+W")) - } - NativeMenuItem::Quit => { - make_menu_item("Quit", selector("terminate:"), Some("Command+Q")) - } - NativeMenuItem::Hide => make_menu_item("Hide", selector("hide:"), Some("Command+H")), + NativeMenuItem::CloseWindow => make_menu_item( + "Close Window", + selector("performClose:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyW)), + ), + NativeMenuItem::Quit => make_menu_item( + "Quit", + selector("terminate:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyQ)), + ), + NativeMenuItem::Hide => make_menu_item( + "Hide", + selector("hide:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyH)), + ), NativeMenuItem::HideOthers => make_menu_item( "Hide Others", selector("hideOtherApplications:"), - Some("Alt+H"), + Some(Accelerator::new(RawMods::Alt, Code::KeyH)), ), NativeMenuItem::ShowAll => { make_menu_item("Show All", selector("unhideAllApplications:"), None) @@ -128,24 +138,44 @@ impl Submenu { NativeMenuItem::ToggleFullScreen => make_menu_item( "Toggle Full Screen", selector("toggleFullScreen:"), - Some("Ctrl+F"), + Some(Accelerator::new(RawMods::Ctrl, Code::KeyF)), ), NativeMenuItem::Minimize => make_menu_item( "Minimize", selector("performMiniaturize:"), - Some("Command+M"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyM)), ), NativeMenuItem::Zoom => make_menu_item("Zoom", selector("performZoom:"), None), - NativeMenuItem::Copy => make_menu_item("Copy", selector("copy:"), Some("Command+C")), - NativeMenuItem::Cut => make_menu_item("Cut", selector("cut:"), Some("Command+X")), - NativeMenuItem::Paste => make_menu_item("Paste", selector("paste:"), Some("Command+V")), - NativeMenuItem::Undo => make_menu_item("Undo", selector("undo:"), Some("Command+Z")), - NativeMenuItem::Redo => { - make_menu_item("Redo", selector("redo:"), Some("Command+Shift+Z")) - } - NativeMenuItem::SelectAll => { - make_menu_item("Select All", selector("selectAll:"), Some("Command+A")) - } + NativeMenuItem::Copy => make_menu_item( + "Copy", + selector("copy:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyC)), + ), + NativeMenuItem::Cut => make_menu_item( + "Cut", + selector("cut:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyX)), + ), + NativeMenuItem::Paste => make_menu_item( + "Paste", + selector("paste:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyV)), + ), + NativeMenuItem::Undo => make_menu_item( + "Undo", + selector("undo:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyZ)), + ), + NativeMenuItem::Redo => make_menu_item( + "Redo", + selector("redo:"), + Some(Accelerator::new(SysMods::CmdShift, Code::KeyZ)), + ), + NativeMenuItem::SelectAll => make_menu_item( + "Select All", + selector("selectAll:"), + Some(Accelerator::new(SysMods::Cmd, Code::KeyA)), + ), NativeMenuItem::Services => unsafe { let (_, item) = make_menu_item("Services", sel!(fireMenubarAction:), None); let app_class = class!(NSApplication); @@ -165,7 +195,7 @@ impl Submenu { label: S, enabled: bool, checked: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> CheckMenuItem { let item = CheckMenuItem::new( label, diff --git a/src/platform_impl/windows/accelerator.rs b/src/platform_impl/windows/accelerator.rs index c4703d3..408775f 100644 --- a/src/platform_impl/windows/accelerator.rs +++ b/src/platform_impl/windows/accelerator.rs @@ -1,115 +1,243 @@ -use windows_sys::Win32::UI::WindowsAndMessaging::{FALT, FCONTROL, FSHIFT, FVIRTKEY}; +use std::fmt; -/// Returns a tuple of (Key, Modifier, a string representation to be used in menu items) -pub fn parse_accelerator>(accelerator: S) -> (u16, u32, String) { - let accelerator = accelerator.as_ref(); - let mut s = accelerator.split("+"); - let count = s.clone().count(); - let (mod1, mod2, key) = { - if count == 2 { - (s.next().unwrap(), None, s.next().unwrap()) - } else if count == 3 { - ( - s.next().unwrap(), - Some(s.next().unwrap()), - s.next().unwrap(), - ) - } else { - panic!("Unsupported accelerator format: {}", accelerator) +use keyboard_types::{Code, Modifiers}; +use windows_sys::Win32::UI::{ + Input::KeyboardAndMouse::*, + WindowsAndMessaging::{ACCEL, FALT, FCONTROL, FSHIFT, FVIRTKEY}, +}; + +use crate::accelerator::Accelerator; + +impl Accelerator { + // Convert a hotkey to an accelerator. + pub fn to_accel(&self, menu_id: u16) -> ACCEL { + let mut virt_key = FVIRTKEY; + let key_mods: Modifiers = self.mods; + if key_mods.contains(Modifiers::CONTROL) { + virt_key |= FCONTROL; + } + if key_mods.contains(Modifiers::ALT) { + virt_key |= FALT; + } + if key_mods.contains(Modifiers::SHIFT) { + virt_key |= FSHIFT; } - }; - let mut accel_str = String::new(); - let mut mods_vk = FVIRTKEY; + let vk_code = key_to_vk(&self.key); + let mod_code = vk_code >> 8; + if mod_code & 0x1 != 0 { + virt_key |= FSHIFT; + } + if mod_code & 0x02 != 0 { + virt_key |= FCONTROL; + } + if mod_code & 0x04 != 0 { + virt_key |= FALT; + } + let raw_key = vk_code & 0x00ff; - let (mod1_vk, mod1_str) = parse_mod(mod1); - accel_str.push_str(mod1_str); - accel_str.push_str("+"); - mods_vk |= mod1_vk; - if let Some(mod2) = mod2 { - let (mod2_vk, mod2_str) = parse_mod(mod2); - accel_str.push_str(mod2_str); - accel_str.push_str("+"); - mods_vk |= mod2_vk; - } - let (key_vk, key_str) = parse_key(key); - accel_str.push_str(key_str); - - (key_vk, mods_vk, accel_str) -} - -fn parse_mod(modifier: &str) -> (u32, &str) { - match modifier.to_uppercase().as_str() { - "SHIFT" => (FSHIFT, "Shift"), - "CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => (FCONTROL, "Ctrl"), - "ALT" => (FALT, "Alt"), - _ => panic!("Unsupported modifier: {}", modifier), + ACCEL { + fVirt: virt_key as u8, + key: raw_key as u16, + cmd: menu_id, + } } } -fn parse_key(key: &str) -> (u16, &str) { - match key.to_uppercase().as_str() { - "SPACE" => (0x20, "Space"), - "BACKSPACE" => (0x08, "Backspace"), - "TAB" => (0x09, "Tab"), - "ENTER" | "RETURN" => (0x0D, "Enter"), - "CAPSLOCK" => (0x14, "Caps Lock"), - "ESC" | "ESCAPE" => (0x1B, "Esc"), - "PAGEUP" => (0x21, "Page Up"), - "PAGEDOWN" => (0x22, "Page Down"), - "END" => (0x23, "End"), - "HOME" => (0x24, "Home"), - "LEFTARROW" => (0x25, "Left Arrow"), - "UPARROW" => (0x26, "Up Arrow"), - "RIGHTARROW" => (0x27, "Right Arrow"), - "DOWNARROW" => (0x28, "Down Arrow"), - "DELETE" => (0x2E, "Del"), - "0" => (0x30, "0"), - "1" => (0x31, "1"), - "2" => (0x32, "2"), - "3" => (0x33, "3"), - "4" => (0x34, "4"), - "5" => (0x35, "5"), - "6" => (0x36, "6"), - "7" => (0x37, "7"), - "8" => (0x38, "8"), - "9" => (0x39, "9"), - "A" => (0x41, "A"), - "B" => (0x42, "B"), - "C" => (0x43, "C"), - "D" => (0x44, "D"), - "E" => (0x45, "E"), - "F" => (0x46, "F"), - "G" => (0x47, "G"), - "H" => (0x48, "H"), - "I" => (0x49, "I"), - "J" => (0x4A, "J"), - "K" => (0x4B, "K"), - "L" => (0x4C, "L"), - "M" => (0x4D, "M"), - "N" => (0x4E, "N"), - "O" => (0x4F, "O"), - "P" => (0x50, "P"), - "Q" => (0x51, "Q"), - "R" => (0x52, "R"), - "S" => (0x53, "S"), - "T" => (0x54, "T"), - "U" => (0x55, "U"), - "V" => (0x56, "V"), - "W" => (0x57, "W"), - "X" => (0x58, "X"), - "Y" => (0x59, "Y"), - "Z" => (0x5A, "Z"), - "NUM0" | "NUMPAD0" => (0x60, "Num 0"), - "NUM1" | "NUMPAD1" => (0x61, "Num 1"), - "NUM2" | "NUMPAD2" => (0x62, "Num 2"), - "NUM3" | "NUMPAD3" => (0x63, "Num 3"), - "NUM4" | "NUMPAD4" => (0x64, "Num 4"), - "NUM5" | "NUMPAD5" => (0x65, "Num 5"), - "NUM6" | "NUMPAD6" => (0x66, "Num 6"), - "NUM7" | "NUMPAD7" => (0x67, "Num 7"), - "NUM8" | "NUMPAD8" => (0x68, "Num 8"), - "NUM9" | "NUMPAD9" => (0x69, "Num 9"), - _ => panic!("Unsupported modifier: {}", key), +// used to build accelerators table from Key +fn key_to_vk(key: &Code) -> VIRTUAL_KEY { + match key { + Code::KeyA => unsafe { VkKeyScanW('a' as u16) as u16 }, + Code::KeyB => unsafe { VkKeyScanW('b' as u16) as u16 }, + Code::KeyC => unsafe { VkKeyScanW('c' as u16) as u16 }, + Code::KeyD => unsafe { VkKeyScanW('d' as u16) as u16 }, + Code::KeyE => unsafe { VkKeyScanW('e' as u16) as u16 }, + Code::KeyF => unsafe { VkKeyScanW('f' as u16) as u16 }, + Code::KeyG => unsafe { VkKeyScanW('g' as u16) as u16 }, + Code::KeyH => unsafe { VkKeyScanW('h' as u16) as u16 }, + Code::KeyI => unsafe { VkKeyScanW('i' as u16) as u16 }, + Code::KeyJ => unsafe { VkKeyScanW('j' as u16) as u16 }, + Code::KeyK => unsafe { VkKeyScanW('k' as u16) as u16 }, + Code::KeyL => unsafe { VkKeyScanW('l' as u16) as u16 }, + Code::KeyM => unsafe { VkKeyScanW('m' as u16) as u16 }, + Code::KeyN => unsafe { VkKeyScanW('n' as u16) as u16 }, + Code::KeyO => unsafe { VkKeyScanW('o' as u16) as u16 }, + Code::KeyP => unsafe { VkKeyScanW('p' as u16) as u16 }, + Code::KeyQ => unsafe { VkKeyScanW('q' as u16) as u16 }, + Code::KeyR => unsafe { VkKeyScanW('r' as u16) as u16 }, + Code::KeyS => unsafe { VkKeyScanW('s' as u16) as u16 }, + Code::KeyT => unsafe { VkKeyScanW('t' as u16) as u16 }, + Code::KeyU => unsafe { VkKeyScanW('u' as u16) as u16 }, + Code::KeyV => unsafe { VkKeyScanW('v' as u16) as u16 }, + Code::KeyW => unsafe { VkKeyScanW('w' as u16) as u16 }, + Code::KeyX => unsafe { VkKeyScanW('x' as u16) as u16 }, + Code::KeyY => unsafe { VkKeyScanW('y' as u16) as u16 }, + Code::KeyZ => unsafe { VkKeyScanW('z' as u16) as u16 }, + Code::Digit0 => unsafe { VkKeyScanW('0' as u16) as u16 }, + Code::Digit1 => unsafe { VkKeyScanW('1' as u16) as u16 }, + Code::Digit2 => unsafe { VkKeyScanW('2' as u16) as u16 }, + Code::Digit3 => unsafe { VkKeyScanW('3' as u16) as u16 }, + Code::Digit4 => unsafe { VkKeyScanW('4' as u16) as u16 }, + Code::Digit5 => unsafe { VkKeyScanW('5' as u16) as u16 }, + Code::Digit6 => unsafe { VkKeyScanW('6' as u16) as u16 }, + Code::Digit7 => unsafe { VkKeyScanW('7' as u16) as u16 }, + Code::Digit8 => unsafe { VkKeyScanW('8' as u16) as u16 }, + Code::Digit9 => unsafe { VkKeyScanW('9' as u16) as u16 }, + Code::Comma => VK_OEM_COMMA, + Code::Minus => VK_OEM_MINUS, + Code::Period => VK_OEM_PERIOD, + Code::Equal => unsafe { VkKeyScanW('=' as u16) as u16 }, + Code::Semicolon => unsafe { VkKeyScanW(';' as u16) as u16 }, + Code::Slash => unsafe { VkKeyScanW('/' as u16) as u16 }, + Code::Backslash => unsafe { VkKeyScanW('\\' as u16) as u16 }, + Code::Quote => unsafe { VkKeyScanW('\'' as u16) as u16 }, + Code::Backquote => unsafe { VkKeyScanW('`' as u16) as u16 }, + Code::BracketLeft => unsafe { VkKeyScanW('[' as u16) as u16 }, + Code::BracketRight => unsafe { VkKeyScanW(']' as u16) as u16 }, + Code::Backspace => VK_BACK, + Code::Tab => VK_TAB, + Code::Space => VK_SPACE, + Code::Enter => VK_RETURN, + Code::Pause => VK_PAUSE, + Code::CapsLock => VK_CAPITAL, + Code::KanaMode => VK_KANA, + Code::Escape => VK_ESCAPE, + Code::NonConvert => VK_NONCONVERT, + Code::PageUp => VK_PRIOR, + Code::PageDown => VK_NEXT, + Code::End => VK_END, + Code::Home => VK_HOME, + Code::ArrowLeft => VK_LEFT, + Code::ArrowUp => VK_UP, + Code::ArrowRight => VK_RIGHT, + Code::ArrowDown => VK_DOWN, + Code::PrintScreen => VK_SNAPSHOT, + Code::Insert => VK_INSERT, + Code::Delete => VK_DELETE, + Code::Help => VK_HELP, + Code::ContextMenu => VK_APPS, + Code::F1 => VK_F1, + Code::F2 => VK_F2, + Code::F3 => VK_F3, + Code::F4 => VK_F4, + Code::F5 => VK_F5, + Code::F6 => VK_F6, + Code::F7 => VK_F7, + Code::F8 => VK_F8, + Code::F9 => VK_F9, + Code::F10 => VK_F10, + Code::F11 => VK_F11, + Code::F12 => VK_F12, + Code::F13 => VK_F13, + Code::F14 => VK_F14, + Code::F15 => VK_F15, + Code::F16 => VK_F16, + Code::F17 => VK_F17, + Code::F18 => VK_F18, + Code::F19 => VK_F19, + Code::F20 => VK_F20, + Code::F21 => VK_F21, + Code::F22 => VK_F22, + Code::F23 => VK_F23, + Code::F24 => VK_F24, + Code::NumLock => VK_NUMLOCK, + Code::ScrollLock => VK_SCROLL, + Code::BrowserBack => VK_BROWSER_BACK, + Code::BrowserForward => VK_BROWSER_FORWARD, + Code::BrowserRefresh => VK_BROWSER_REFRESH, + Code::BrowserStop => VK_BROWSER_STOP, + Code::BrowserSearch => VK_BROWSER_SEARCH, + Code::BrowserFavorites => VK_BROWSER_FAVORITES, + Code::BrowserHome => VK_BROWSER_HOME, + Code::AudioVolumeMute => VK_VOLUME_MUTE, + Code::AudioVolumeDown => VK_VOLUME_DOWN, + Code::AudioVolumeUp => VK_VOLUME_UP, + Code::MediaTrackNext => VK_MEDIA_NEXT_TRACK, + Code::MediaTrackPrevious => VK_MEDIA_PREV_TRACK, + Code::MediaStop => VK_MEDIA_STOP, + Code::MediaPlayPause => VK_MEDIA_PLAY_PAUSE, + Code::LaunchMail => VK_LAUNCH_MAIL, + Code::Convert => VK_CONVERT, + key => panic!("Unsupported modifier: {}", key), + } +} + +impl fmt::Display for Accelerator { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let key_mods: Modifiers = self.mods; + if key_mods.contains(Modifiers::CONTROL) { + write!(f, "Ctrl+")?; + } + if key_mods.contains(Modifiers::SHIFT) { + write!(f, "Shift+")?; + } + if key_mods.contains(Modifiers::ALT) { + write!(f, "Alt+")?; + } + if key_mods.contains(Modifiers::SUPER) { + write!(f, "Windows+")?; + } + match &self.key { + Code::KeyA => write!(f, "A"), + Code::KeyB => write!(f, "B"), + Code::KeyC => write!(f, "C"), + Code::KeyD => write!(f, "D"), + Code::KeyE => write!(f, "E"), + Code::KeyF => write!(f, "F"), + Code::KeyG => write!(f, "G"), + Code::KeyH => write!(f, "H"), + Code::KeyI => write!(f, "I"), + Code::KeyJ => write!(f, "J"), + Code::KeyK => write!(f, "K"), + Code::KeyL => write!(f, "L"), + Code::KeyM => write!(f, "M"), + Code::KeyN => write!(f, "N"), + Code::KeyO => write!(f, "O"), + Code::KeyP => write!(f, "P"), + Code::KeyQ => write!(f, "Q"), + Code::KeyR => write!(f, "R"), + Code::KeyS => write!(f, "S"), + Code::KeyT => write!(f, "T"), + Code::KeyU => write!(f, "U"), + Code::KeyV => write!(f, "V"), + Code::KeyW => write!(f, "W"), + Code::KeyX => write!(f, "X"), + Code::KeyY => write!(f, "Y"), + Code::KeyZ => write!(f, "Z"), + Code::Digit0 => write!(f, "0"), + Code::Digit1 => write!(f, "1"), + Code::Digit2 => write!(f, "2"), + Code::Digit3 => write!(f, "3"), + Code::Digit4 => write!(f, "4"), + Code::Digit5 => write!(f, "5"), + Code::Digit6 => write!(f, "6"), + Code::Digit7 => write!(f, "7"), + Code::Digit8 => write!(f, "8"), + Code::Digit9 => write!(f, "9"), + Code::Comma => write!(f, ","), + Code::Minus => write!(f, "-"), + Code::Period => write!(f, "."), + Code::Space => write!(f, "Space"), + Code::Equal => write!(f, "="), + Code::Semicolon => write!(f, ";"), + Code::Slash => write!(f, "/"), + Code::Backslash => write!(f, "\\"), + Code::Quote => write!(f, "\'"), + Code::Backquote => write!(f, "`"), + Code::BracketLeft => write!(f, "["), + Code::BracketRight => write!(f, "]"), + Code::Tab => write!(f, "Tab"), + Code::Escape => write!(f, "Esc"), + Code::Delete => write!(f, "Del"), + Code::Insert => write!(f, "Ins"), + Code::PageUp => write!(f, "PgUp"), + Code::PageDown => write!(f, "PgDn"), + // These names match LibreOffice. + Code::ArrowLeft => write!(f, "Left"), + Code::ArrowRight => write!(f, "Right"), + Code::ArrowUp => write!(f, "Up"), + Code::ArrowDown => write!(f, "Down"), + _ => write!(f, "{:?}", self.key), + } } } diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 4cfc964..7aa4c38 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -3,7 +3,7 @@ mod accelerator; mod util; -use crate::{counter::Counter, NativeMenuItem}; +use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem}; use once_cell::sync::Lazy; use std::{cell::RefCell, collections::HashMap, rc::Rc}; use util::{decode_wide, encode_wide, LOWORD}; @@ -23,8 +23,6 @@ use windows_sys::Win32::{ }, }; -use self::accelerator::parse_accelerator; - const COUNTER_START: u64 = 1000; static COUNTER: Counter = Counter::new_with_start(COUNTER_START); @@ -195,7 +193,7 @@ impl Submenu { &mut self, label: S, enabled: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> MenuItem { let id = COUNTER.next(); let mut flags = MF_STRING; @@ -205,12 +203,8 @@ impl Submenu { let mut label = label.as_ref().to_string(); if let Some(accelerator) = accelerator { - let (key, mods, accel_str) = parse_accelerator(accelerator); - let accel = ACCEL { - key, - fVirt: mods as _, - cmd: id as _, - }; + let accel_str = accelerator.to_string(); + let accel = accelerator.to_accel(id as u16); label.push_str("\t"); label.push_str(&accel_str); @@ -268,7 +262,7 @@ impl Submenu { label: S, enabled: bool, checked: bool, - accelerator: Option<&str>, + accelerator: Option, ) -> CheckMenuItem { let mut item = CheckMenuItem(self.add_item(label, enabled, accelerator)); item.set_checked(checked);