diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index fec6f150f6c341f916e0173379aba63bebcc1ffd..8d4dc371e6a3bfd560046db2f562cb9f8ebf991d 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -425,6 +425,10 @@ impl AppContext { .collect() } + pub fn active_window(&self) -> Option { + self.platform.active_window() + } + /// Opens a new window with the given option and the root view returned by the given function. /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific /// functionality. @@ -1015,6 +1019,42 @@ impl AppContext { activate(); subscription } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + for window in self.windows() { + window + .update(self, |_, cx| { + cx.window + .current_frame + .dispatch_tree + .clear_pending_keystrokes() + }) + .ok(); + } + } + + pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + if let Some(window) = self.active_window() { + let window_action_available = window + .update(self, |_, cx| { + if let Some(focus_id) = cx.window.focus { + cx.window + .current_frame + .dispatch_tree + .is_action_available(action, focus_id) + } else { + false + } + }) + .unwrap_or(false); + if window_action_available { + return true; + } + } + + self.global_action_listeners + .contains_key(&action.as_any().type_id()) + } } impl Context for AppContext { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 4838b1a612ce65ba33c03ac25da878a752f716d3..0df052dfdf9e0f066666f56a94295d803dfab613 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -82,13 +82,13 @@ impl DispatchTree { } } - pub fn clear_keystroke_matchers(&mut self) { + pub fn clear_pending_keystrokes(&mut self) { self.keystroke_matchers.clear(); } /// Preserve keystroke matchers from previous frames to support multi-stroke /// bindings across multiple frames. - pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option) { + pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option) { if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) { let dispatch_path = self.dispatch_path(node_id); @@ -163,6 +163,22 @@ impl DispatchTree { actions } + pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool { + if let Some(node) = self.focusable_node_ids.get(&target) { + for node_id in self.dispatch_path(*node) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; + } + } + } + false + } + pub fn bindings_for_action( &self, action: &dyn Action, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 40c555301bdf00cbad7d2ef36b2c99511daa6d90..5d3a92f052eb8eafebe7e93b2d47e82c586c270a 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -1,3 +1,4 @@ +mod app_menu; mod keystroke; #[cfg(target_os = "macos")] mod mac; @@ -32,6 +33,7 @@ use std::{ }; use uuid::Uuid; +pub use app_menu::*; pub use keystroke::*; #[cfg(target_os = "macos")] pub use mac::*; @@ -59,7 +61,7 @@ pub trait Platform: 'static { fn displays(&self) -> Vec>; fn display(&self, id: DisplayId) -> Option>; - fn main_window(&self) -> Option; + fn active_window(&self) -> Option; fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..0f784f6585f9233c953bad50dbd6d6a2bc3207f9 --- /dev/null +++ b/crates/gpui2/src/platform/app_menu.rs @@ -0,0 +1,96 @@ +use crate::{Action, AppContext, Platform}; +use util::ResultExt; + +pub struct Menu<'a> { + pub name: &'a str, + pub items: Vec>, +} + +pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), + Action { + name: &'a str, + action: Box, + os_action: Option, + }, +} + +impl<'a> MenuItem<'a> { + pub fn separator() -> Self { + Self::Separator + } + + pub fn submenu(menu: Menu<'a>) -> Self { + Self::Submenu(menu) + } + + pub fn action(name: &'a str, action: impl Action) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: None, + } + } + + pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: Some(os_action), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum OsAction { + Cut, + Copy, + Paste, + SelectAll, + Undo, + Redo, +} + +pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { + platform.on_will_open_menu(Box::new({ + let cx = cx.to_async(); + move || { + cx.update(|cx| cx.clear_pending_keystrokes()).ok(); + } + })); + + platform.on_validate_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.is_action_available(action)) + .unwrap_or(false) + } + })); + + platform.on_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| { + // if let Some(main_window) = cx.active_window() { + // let dispatched = main_window + // .update(&mut *cx, |cx| { + // if let Some(view_id) = cx.focused_view_id() { + // cx.dispatch_action(Some(view_id), action); + // true + // } else { + // false + // } + // }) + // .unwrap_or(false); + + // if dispatched { + // return; + // } + // } + // cx.dispatch_global_action_any(action); + }) + .log_err(); + } + })); +} diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 9d02c8fb938e9b6f4cd1014e5ebce4f037911bcd..6dae0afc0e59af2abaeba861bb6c22638d3b0847 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,16 +1,17 @@ use super::BoolExt; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, - MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, - Result, SemanticVersion, VideoTimestamp, WindowOptions, + ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker, + MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow, + NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, + NSSavePanel, NSWindow, }, base::{id, nil, BOOL, YES}, foundation::{ @@ -237,114 +238,115 @@ impl MacPlatform { // application_menu // } - // unsafe fn create_menu_item( - // &self, - // item: MenuItem, - // delegate: id, - // actions: &mut Vec>, - // keystroke_matcher: &KeymapMatcher, - // ) -> id { - // match item { - // MenuItem::Separator => NSMenuItem::separatorItem(nil), - // MenuItem::Action { - // name, - // action, - // os_action, - // } => { - // // TODO - // let keystrokes = keystroke_matcher - // .bindings_for_action(action.id()) - // .find(|binding| binding.action().eq(action.as_ref())) - // .map(|binding| binding.keystrokes()); - // let selector = match os_action { - // Some(crate::OsAction::Cut) => selector("cut:"), - // Some(crate::OsAction::Copy) => selector("copy:"), - // Some(crate::OsAction::Paste) => selector("paste:"), - // Some(crate::OsAction::SelectAll) => selector("selectAll:"), - // Some(crate::OsAction::Undo) => selector("undo:"), - // Some(crate::OsAction::Redo) => selector("redo:"), - // None => selector("handleGPUIMenuItem:"), - // }; - - // 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), - // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), - // ] { - // if *modifier { - // mask |= *flag; - // } - // } - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // 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, - // ns_string(""), - // ) - // .autorelease(); - // } - // } else { - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // 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 - // } - // } - // } + unsafe fn create_menu_item( + &self, + item: MenuItem, + delegate: id, + actions: &mut Vec>, + keystroke_matcher: &KeystrokeMatcher, + ) -> id { + todo!() + // match item { + // MenuItem::Separator => NSMenuItem::separatorItem(nil), + // MenuItem::Action { + // name, + // action, + // os_action, + // } => { + // // TODO + // let keystrokes = keystroke_matcher + // .bindings_for_action(action.id()) + // .find(|binding| binding.action().eq(action.as_ref())) + // .map(|binding| binding.keystrokes()); + // let selector = match os_action { + // Some(crate::OsAction::Cut) => selector("cut:"), + // Some(crate::OsAction::Copy) => selector("copy:"), + // Some(crate::OsAction::Paste) => selector("paste:"), + // Some(crate::OsAction::SelectAll) => selector("selectAll:"), + // Some(crate::OsAction::Undo) => selector("undo:"), + // Some(crate::OsAction::Redo) => selector("redo:"), + // None => selector("handleGPUIMenuItem:"), + // }; + + // 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), + // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), + // ] { + // if *modifier { + // mask |= *flag; + // } + // } + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // 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, + // ns_string(""), + // ) + // .autorelease(); + // } + // } else { + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // 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 + // } + // } + } } impl Platform for MacPlatform { @@ -479,8 +481,8 @@ impl Platform for MacPlatform { MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) } - fn main_window(&self) -> Option { - MacWindow::main_window() + fn active_window(&self) -> Option { + MacWindow::active_window() } fn open_window( diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index 5b72c10851ff555b08669d8db96e143509e8ad46..ba9a67e1580c16dfdbfc0849d8d1678af308c96a 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -662,7 +662,7 @@ impl MacWindow { } } - pub fn main_window() -> Option { + pub fn active_window() -> Option { unsafe { let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 6fa706f617119bfad728078a378309c16ab19df4..264273730510365d319120e3faaa0efd35076a1d 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -127,7 +127,7 @@ impl Platform for TestPlatform { self.displays().iter().find(|d| d.id() == id).cloned() } - fn main_window(&self) -> Option { + fn active_window(&self) -> Option { unimplemented!() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..8645554e5af83600b6ff6438869048fee09b46d1 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> { self.window .current_frame .dispatch_tree - .clear_keystroke_matchers(); + .clear_pending_keystrokes(); self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(focus_id), @@ -1177,7 +1177,7 @@ impl<'a> WindowContext<'a> { self.window .current_frame .dispatch_tree - .preserve_keystroke_matchers( + .preserve_pending_keystrokes( &mut self.window.previous_frame.dispatch_tree, self.window.focus, );