Enable and disable application menu items based on the active view

Max Brunsfeld created

Change summary

crates/gpui/src/app.rs                   | 27 +++++++++++++++++++++++++
crates/gpui/src/platform.rs              |  1 
crates/gpui/src/platform/mac/platform.rs | 26 +++++++++++++++++++++++++
crates/gpui/src/platform/test.rs         |  2 +
4 files changed, 56 insertions(+)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -192,6 +192,13 @@ impl App {
                 cx.borrow_mut().quit();
             }
         }));
+        foreground_platform.on_validate_menu_command(Box::new({
+            let cx = app.0.clone();
+            move |action| {
+                let cx = cx.borrow_mut();
+                cx.is_action_available(action)
+            }
+        }));
         foreground_platform.on_menu_command(Box::new({
             let cx = app.0.clone();
             move |action| {
@@ -1364,6 +1371,26 @@ impl MutableAppContext {
             })
     }
 
+    pub fn is_action_available(&self, action: &dyn Action) -> bool {
+        let action_type = action.as_any().type_id();
+        if let Some(window_id) = self.cx.platform.key_window_id() {
+            if let Some((presenter, _)) = self.presenters_and_platform_windows.get(&window_id) {
+                let dispatch_path = presenter.borrow().dispatch_path(&self.cx);
+                for view_id in dispatch_path {
+                    if let Some(view) = self.views.get(&(window_id, view_id)) {
+                        let view_type = view.as_any().type_id();
+                        if let Some(actions) = self.actions.get(&view_type) {
+                            if actions.contains_key(&action_type) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        self.global_actions.contains_key(&action_type)
+    }
+
     pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
         let presenter = self
             .presenters_and_platform_windows

crates/gpui/src/platform.rs 🔗

@@ -73,6 +73,7 @@ pub(crate) trait ForegroundPlatform {
     fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
 
     fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
+    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
     fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
     fn prompt_for_paths(
         &self,

crates/gpui/src/platform/mac/platform.rs 🔗

@@ -89,6 +89,10 @@ unsafe fn build_classes() {
             sel!(handleGPUIMenuItem:),
             handle_menu_item as extern "C" fn(&mut Object, Sel, id),
         );
+        decl.add_method(
+            sel!(validateMenuItem:),
+            validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
+        );
         decl.add_method(
             sel!(application:openURLs:),
             open_urls as extern "C" fn(&mut Object, Sel, id, id),
@@ -107,6 +111,7 @@ pub struct MacForegroundPlatformState {
     quit: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
     menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+    validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
     open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
     menu_actions: Vec<Box<dyn Action>>,
@@ -237,6 +242,10 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().menu_command = Some(callback);
     }
 
+    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+        self.0.borrow_mut().validate_menu_command = Some(callback);
+    }
+
     fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
         unsafe {
             let app: id = msg_send![APP_CLASS, sharedApplication];
@@ -738,6 +747,23 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
     }
 }
 
+extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
+    unsafe {
+        let mut result = false;
+        let platform = get_foreground_platform(this);
+        let mut platform = platform.0.borrow_mut();
+        if let Some(mut callback) = platform.validate_menu_command.take() {
+            let tag: NSInteger = msg_send![item, tag];
+            let index = tag as usize;
+            if let Some(action) = platform.menu_actions.get(index) {
+                result = callback(action.as_ref());
+            }
+            platform.validate_menu_command = Some(callback);
+        }
+        result
+    }
+}
+
 unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }

crates/gpui/src/platform/test.rs 🔗

@@ -74,6 +74,8 @@ impl super::ForegroundPlatform for ForegroundPlatform {
 
     fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
 
+    fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
+
     fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
 
     fn prompt_for_paths(