From a3b17ffd15b1c2658e8f56a0179c4529e5b46a35 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Jun 2022 17:47:39 -0700 Subject: [PATCH] Allow creating application menus with submenus --- crates/gpui/src/app.rs | 3 +- crates/gpui/src/platform/mac/platform.rs | 185 ++++++++++++++--------- 2 files changed, 119 insertions(+), 69 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3c04ea1696..e5b0f71ff3 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -133,11 +133,12 @@ pub struct Menu<'a> { } pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), Action { name: &'a str, action: Box, }, - Separator, } #[derive(Clone)] diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 2be0cdc454..5dc10c7b57 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -127,89 +127,131 @@ impl MacForegroundPlatform { &self, menus: Vec, delegate: id, + actions: &mut Vec>, keystroke_matcher: &keymap::Matcher, ) -> id { - let menu_bar = NSMenu::new(nil).autorelease(); - menu_bar.setDelegate_(delegate); - let mut state = self.0.borrow_mut(); - - state.menu_actions.clear(); + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); for menu_config in menus { - let menu_bar_item = NSMenuItem::new(nil).autorelease(); let menu = NSMenu::new(nil).autorelease(); - let menu_name = menu_config.name; - - menu.setTitle_(ns_string(menu_name)); + menu.setTitle_(ns_string(menu_config.name)); menu.setDelegate_(delegate); for item_config in menu_config.items { - let item; - - match item_config { - MenuItem::Separator => { - item = NSMenuItem::separatorItem(nil); - } - MenuItem::Action { name, action } => { - let mut keystroke = None; - if let Some(binding) = keystroke_matcher - .bindings_for_action_type(action.as_any().type_id()) - .find(|binding| binding.action().eq(action.as_ref())) - { - if binding.keystrokes().len() == 1 { - keystroke = binding.keystrokes().first() - } - } - - if let Some(keystroke) = keystroke { - let mut mask = NSEventModifierFlags::empty(); - for (modifier, flag) in &[ - (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), - (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), - (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), - ] { - if *modifier { - mask |= *flag; - } - } - - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector("handleGPUIMenuItem:"), - ns_string(key_to_native(&keystroke.key).as_ref()), - ) - .autorelease(); - item.setKeyEquivalentModifierMask_(mask); - } else { - item = NSMenuItem::alloc(nil) - .initWithTitle_action_keyEquivalent_( - ns_string(name), - selector("handleGPUIMenuItem:"), - ns_string(""), - ) - .autorelease(); - } - - let tag = state.menu_actions.len() as NSInteger; - let _: () = msg_send![item, setTag: tag]; - state.menu_actions.push(action); - } - } - - menu.addItem_(item); + menu.addItem_(self.create_menu_item( + item_config, + delegate, + actions, + keystroke_matcher, + )); } - menu_bar_item.setSubmenu_(menu); - menu_bar.addItem_(menu_bar_item); + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); - if menu_name == "Window" { + if menu_config.name == "Window" { let app: id = msg_send![APP_CLASS, sharedApplication]; app.setWindowsMenu_(menu); } } - menu_bar + application_menu + } + + unsafe fn create_menu_item( + &self, + item: MenuItem, + delegate: id, + actions: &mut Vec>, + keystroke_matcher: &keymap::Matcher, + ) -> id { + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { name, action } => { + let keystrokes = keystroke_matcher + .bindings_for_action_type(action.as_any().type_id()) + .find(|binding| binding.action().eq(action.as_ref())) + .map(|binding| binding.keystrokes()); + + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), + (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), + (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + ] { + if *modifier { + mask |= *flag; + } + } + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector("handleGPUIMenuItem:"), + ns_string(key_to_native(&keystroke.key).as_ref()), + ) + .autorelease(); + item.setKeyEquivalentModifierMask_(mask); + } + // For multi-keystroke bindings, render the keystroke as part of the title. + else { + use std::fmt::Write; + + let mut name = format!("{name} ["); + for (i, keystroke) in keystrokes.iter().enumerate() { + if i > 0 { + name.push(' '); + } + write!(&mut name, "{}", keystroke).unwrap(); + } + name.push(']'); + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(&name), + selector("handleGPUIMenuItem:"), + ns_string(""), + ) + .autorelease(); + } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector("handleGPUIMenuItem:"), + ns_string(""), + ) + .autorelease(); + } + + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action); + item + } + MenuItem::Submenu(Menu { name, items }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(self.create_menu_item( + item, + delegate, + actions, + keystroke_matcher, + )); + } + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + item + } + } } } @@ -270,7 +312,14 @@ impl platform::ForegroundPlatform for MacForegroundPlatform { fn set_menus(&self, menus: Vec, keystroke_matcher: &keymap::Matcher) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; - app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), keystroke_matcher)); + let mut state = self.0.borrow_mut(); + let actions = &mut state.menu_actions; + app.setMainMenu_(self.create_menu_bar( + menus, + app.delegate(), + actions, + keystroke_matcher, + )); } }