diff --git a/src/platform_impl/macos/accelerator.rs b/src/platform_impl/macos/accelerator.rs new file mode 100644 index 0000000..1e2303a --- /dev/null +++ b/src/platform_impl/macos/accelerator.rs @@ -0,0 +1,110 @@ +use cocoa::appkit::NSEventModifierFlags; + +/// 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) + } + }; + + 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), + } +} diff --git a/src/platform_impl/macos/menu_item.rs b/src/platform_impl/macos/menu_item.rs index f8db622..baae514 100644 --- a/src/platform_impl/macos/menu_item.rs +++ b/src/platform_impl/macos/menu_item.rs @@ -1,6 +1,7 @@ use crate::counter::Counter; +use crate::platform_impl::platform_impl::accelerator::{parse_accelerator, remove_mnemonic}; use cocoa::{ - appkit::NSButton, + appkit::{NSButton, NSEventModifierFlags, NSMenuItem}, base::{id, nil, BOOL, NO, YES}, foundation::NSString, }; @@ -11,12 +12,8 @@ use objc::{ runtime::{Class, Object, Sel}, sel, sel_impl, }; -use std::slice; +use std::rc::Rc; use std::sync::Once; -use std::{ - collections::hash_map::DefaultHasher, - hash::{Hash, Hasher}, -}; static COUNTER: Counter = Counter::new(); @@ -24,11 +21,17 @@ static COUNTER: Counter = Counter::new(); pub struct TextMenuItem { pub(crate) id: u64, pub(crate) ns_menu_item: id, + label: Rc, } impl TextMenuItem { - pub fn new(label: impl AsRef, enabled: bool, selector: Sel) -> Self { - let (id, ns_menu_item) = make_menu_item(label.as_ref(), selector); + pub fn new( + label: impl AsRef, + enabled: bool, + selector: Sel, + accelerator: Option<&str>, + ) -> Self { + let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); unsafe { (&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id); @@ -38,25 +41,23 @@ impl TextMenuItem { let () = msg_send![ns_menu_item, setEnabled: NO]; } } - - Self { id, ns_menu_item } + Self { + id, + ns_menu_item, + label: Rc::from(label.as_ref()), + } } pub fn label(&self) -> String { - unsafe { - let title: id = msg_send![self.ns_menu_item, title]; - let data = title.UTF8String() as *const u8; - let len = title.len(); - - String::from_utf8_lossy(slice::from_raw_parts(data, len)).to_string() - } + self.label.to_string() } pub fn set_label(&mut self, label: impl AsRef) { unsafe { - let title = NSString::alloc(nil).init_str(label.as_ref()); + let title = NSString::alloc(nil).init_str(&remove_mnemonic(&label)); self.ns_menu_item.setTitle_(title); } + self.label = Rc::from(label.as_ref()); } pub fn enabled(&self) -> bool { @@ -81,18 +82,13 @@ impl TextMenuItem { } } -pub fn make_menu_item( - title: &str, - selector: Sel, - //key_equivalent: Option, - //menu_type: MenuType, -) -> (MenuId, *mut Object) { +pub fn make_menu_item(title: &str, selector: Sel, accelerator: Option<&str>) -> (u64, *mut Object) { let alloc = make_menu_item_alloc(); let menu_id = COUNTER.next(); unsafe { let title = NSString::alloc(nil).init_str(title); - let menu_item = make_menu_item_from_alloc(alloc, title, selector); //, key_equivalent, menu_type); + let menu_item = make_menu_item_from_alloc(alloc, title, selector, accelerator); (menu_id, menu_item) } @@ -138,26 +134,26 @@ fn make_menu_item_from_alloc( alloc: *mut Object, title: *mut Object, selector: Sel, - //key_equivalent: Option, - //menu_type: MenuType, + accelerator: Option<&str>, ) -> *mut Object { unsafe { - // let (key, masks) = match key_equivalent { - // Some(ke) => ( - // NSString::alloc(nil).init_str(ke.key), - // ke.masks.unwrap_or_else(NSEventModifierFlags::empty), - // ), - // None => ( - // NSString::alloc(nil).init_str(""), - // NSEventModifierFlags::empty(), - // ), - // }; - let key = NSString::alloc(nil).init_str(""); + let (key_equivalent, masks) = match accelerator { + Some(accelerator) => { + let (key, mods) = parse_accelerator(accelerator); + let key = NSString::alloc(nil).init_str(&key); + (key, mods) + } + None => ( + NSString::alloc(nil).init_str(""), + NSEventModifierFlags::empty(), + ), + }; // allocate our item to our class - let item: id = msg_send![alloc, initWithTitle: title action: selector keyEquivalent: key]; + let item: id = + msg_send![alloc, initWithTitle: title action: selector keyEquivalent: key_equivalent]; + item.setKeyEquivalentModifierMask_(masks); - // item.setKeyEquivalentModifierMask_(masks); item } } diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index cd55b4f..2e3366e 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,3 +1,7 @@ +mod accelerator; +mod menu_item; + +use crate::NativeMenuItem; use cocoa::{ appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, base::{id, nil, NO}, @@ -5,9 +9,9 @@ use cocoa::{ }; use objc::{msg_send, sel, sel_impl}; -mod menu_item; pub use menu_item::TextMenuItem; -use menu_item::*; + +use self::accelerator::remove_mnemonic; #[derive(Debug, Clone)] pub struct Menu(id); @@ -23,7 +27,7 @@ impl Menu { pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { let menu = Menu::new(); - let menu_item = TextMenuItem::new("", enabled, sel!(fireMenubarAction:)); + let menu_item = TextMenuItem::new("", enabled, sel!(fireMenubarAction:), None); unsafe { menu_item.ns_menu_item.setSubmenu_(menu.0); @@ -61,9 +65,10 @@ impl Submenu { } pub fn set_label(&mut self, label: impl AsRef) { - self.menu_item.set_label(label.as_ref().to_string()); + let label = remove_mnemonic(label); + self.menu_item.set_label(&label); unsafe { - let menu_title = NSString::alloc(nil).init_str(label.as_ref()); + let menu_title = NSString::alloc(nil).init_str(&label); let () = msg_send![self.menu.0, setTitle: menu_title]; } } @@ -80,11 +85,21 @@ impl Submenu { self.menu.add_submenu(label, enabled) } - pub fn add_text_item(&mut self, label: impl AsRef, enabled: bool) -> TextMenuItem { - let item = TextMenuItem::new(label, enabled, sel!(fireMenubarAction:)); + pub fn add_text_item( + &mut self, + label: impl AsRef, + enabled: bool, + accelerator: Option<&str>, + ) -> TextMenuItem { + let item = TextMenuItem::new(label, enabled, sel!(fireMenubarAction:), accelerator); unsafe { self.menu.0.addItem_(item.ns_menu_item); } item } + + pub fn add_native_item(&mut self, _item: NativeMenuItem) { + // TODO + return; + } }