diff --git a/Cargo.toml b/Cargo.toml index b1f25da..11c70c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,13 @@ features = [ "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_UI_Shell", - "Win32_Globalization" + "Win32_Globalization", + "Win32_UI_Input_KeyboardAndMouse" ] [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.15" +libxdo = "0.6.0" [target.'cfg(target_os = "macos")'.dependencies] cocoa = "0.24" @@ -33,4 +35,4 @@ objc = "0.2" [dev-dependencies] winit = { git = "https://github.com/rust-windowing/winit" } -tao = { git = "https://github.com/tauri-apps/tao", branch = "muda/disable-gtk-menu-creation" } +tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } diff --git a/examples/tao.rs b/examples/tao.rs index 0509b48..6ad1c0b 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -1,4 +1,4 @@ -use muda::{menu_event_receiver, Menu}; +use muda::{menu_event_receiver, Menu, NativeMenuItem}; #[cfg(target_os = "linux")] use tao::platform::unix::WindowExtUnix; #[cfg(target_os = "windows")] @@ -16,16 +16,19 @@ fn main() { let window2 = WindowBuilder::new().build(&event_loop).unwrap(); let mut menu_bar = Menu::new(); + let mut file_menu = menu_bar.add_submenu("&File", true); - let mut edit_menu = menu_bar.add_submenu("&Edit", true); - let mut open_item = file_menu.add_text_item("&Open", true, None); - let mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S")); - let _quit_item = file_menu.add_text_item("&Quit", true, None); + file_menu.add_native_item(NativeMenuItem::Minimize); + file_menu.add_native_item(NativeMenuItem::CloseWindow); + file_menu.add_native_item(NativeMenuItem::Quit); - let _copy_item = edit_menu.add_text_item("&Copy", true, None); - let _cut_item = edit_menu.add_text_item("C&ut", true, None); + let mut edit_menu = menu_bar.add_submenu("&Edit", true); + edit_menu.add_native_item(NativeMenuItem::Cut); + edit_menu.add_native_item(NativeMenuItem::Copy); + edit_menu.add_native_item(NativeMenuItem::Paste); + edit_menu.add_native_item(NativeMenuItem::SelectAll); #[cfg(target_os = "windows")] { @@ -38,11 +41,6 @@ fn main() { menu_bar.init_for_gtk_window(window2.gtk_window()); } - #[cfg(target_os = "macos")] - { - menu_bar.init_for_nsapp(); - } - let menu_channel = menu_event_receiver(); let mut open_item_disabled = false; let mut counter = 0; @@ -50,6 +48,21 @@ fn main() { event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; + match event { + #[cfg(target_os = "macos")] + Event::NewEvents(tao::event::StartCause::Init) => { + menu_bar.init_for_nsapp(); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::MainEventsCleared => { + // window.request_redraw(); + } + _ => (), + } + if let Ok(event) = menu_channel.try_recv() { match event.id { _ if event.id == save_item.id() => { @@ -66,16 +79,5 @@ fn main() { _ => {} } } - - match event { - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::MainEventsCleared => { - window.request_redraw(); - } - _ => (), - } }) } diff --git a/examples/winit.rs b/examples/winit.rs index cd03036..c46eca3 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,4 +1,4 @@ -use muda::{menu_event_receiver, Menu}; +use muda::{menu_event_receiver, Menu, NativeMenuItem}; #[cfg(target_os = "macos")] use winit::platform::macOS::EventLoopExtMacOS; #[cfg(target_os = "windows")] @@ -36,15 +36,17 @@ fn main() { let _window2 = WindowBuilder::new().build(&event_loop).unwrap(); let mut file_menu = menu_bar.add_submenu("&File", true); - let mut edit_menu = menu_bar.add_submenu("&Edit", true); - - let mut open_item = file_menu.add_text_item("&Open", true, Some("Ctrl+O")); - + let mut open_item = file_menu.add_text_item("&Open", true, None); let mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S")); - let _quit_item = file_menu.add_text_item("&Quit", true, None); + file_menu.add_native_item(NativeMenuItem::Minimize); + file_menu.add_native_item(NativeMenuItem::CloseWindow); + file_menu.add_native_item(NativeMenuItem::Quit); - let _copy_item = edit_menu.add_text_item("&Copy", true, Some("Ctrl+C")); - let _cut_item = edit_menu.add_text_item("C&ut", true, None); + let mut edit_menu = menu_bar.add_submenu("&Edit", true); + edit_menu.add_native_item(NativeMenuItem::Cut); + edit_menu.add_native_item(NativeMenuItem::Copy); + edit_menu.add_native_item(NativeMenuItem::Paste); + edit_menu.add_native_item(NativeMenuItem::SelectAll); #[cfg(target_os = "windows")] { @@ -64,6 +66,21 @@ fn main() { event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; + match event { + #[cfg(target_os = "macos")] + Event::NewEvents(winit::event::StartCause::Init) => { + menu_bar.init_for_nsapp(); + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::MainEventsCleared => { + window.request_redraw(); + } + _ => (), + } + if let Ok(event) = menu_channel.try_recv() { match event.id { _ if event.id == save_item.id() => { @@ -80,20 +97,5 @@ fn main() { _ => {} } } - - match event { - #[cfg(target_os = "macos")] - Event::NewEvents(winit::event::StartCause::Init) => { - menu_bar.init_for_nsapp(); - } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - Event::MainEventsCleared => { - window.request_redraw(); - } - _ => (), - } }) } diff --git a/src/lib.rs b/src/lib.rs index 90d23cc..0aee1b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,7 @@ impl Menu { Submenu(self.0.add_submenu(label, enabled)) } - /// Adds this menu to a [`gtk::Window`] + /// Adds this menu to a [`gtk::ApplicationWindow`] /// /// This method adds a [`gtk::Box`] then adds a [`gtk::MenuBar`] as its first child and returns the [`gtk::Box`]. /// So if more widgets need to be added, then [`gtk::prelude::BoxExt::pack_start`] or @@ -130,6 +130,7 @@ impl Menu { #[cfg(target_os = "linux")] pub fn init_for_gtk_window(&self, w: &W) -> std::rc::Rc where + W: gtk::prelude::IsA, W: gtk::prelude::IsA, W: gtk::prelude::IsA, { @@ -172,15 +173,11 @@ impl Menu { self.0.haccel() } - /// Removes this menu from a [`gtk::Window`] - /// - /// ## Panics: - /// - /// Panics if the window doesn't have a menu created by this crate. + /// Removes this menu from a [`gtk::ApplicationWindow`] #[cfg(target_os = "linux")] pub fn remove_for_gtk_window(&self, w: &W) where - W: gtk::prelude::IsA, + W: gtk::prelude::IsA, W: gtk::prelude::IsA, { self.0.remove_for_gtk_window(w) @@ -192,12 +189,11 @@ impl Menu { self.0.remove_for_hwnd(hwnd) } - /// Hides this menu from a [`gtk::Window`] + /// Hides this menu from a [`gtk::ApplicationWindow`] #[cfg(target_os = "linux")] pub fn hide_for_gtk_window(&self, w: &W) where - W: gtk::prelude::IsA, - W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.0.hide_for_gtk_window(w) } @@ -208,12 +204,11 @@ impl Menu { self.0.hide_for_hwnd(hwnd) } - /// Shows this menu from a [`gtk::Window`] + /// Shows this menu from a [`gtk::ApplicationWindow`] #[cfg(target_os = "linux")] pub fn show_for_gtk_window(&self, w: &W) where - W: gtk::prelude::IsA, - W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.0.show_for_gtk_window(w) } @@ -290,6 +285,10 @@ impl Submenu { ) -> TextMenuItem { TextMenuItem(self.0.add_text_item(label, enabled, accelerator)) } + + pub fn add_native_item(&mut self, item: NativeMenuItem) { + self.0.add_native_item(item) + } } /// This is a Text menu item within a [`Submenu`]. @@ -322,3 +321,126 @@ impl TextMenuItem { self.0.id() } } + +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum NativeMenuItem { + /// A native “About” menu item. + /// + /// The first value is the application name, and the second is its metadata. + /// + /// ## platform-specific: + /// + /// - **macOS**: the metadata is ignore. + /// - **Windows**: Not implemented. + About(String, AboutMetadata), + /// A native “hide the app” menu item. + /// + /// ## platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + Hide, + /// A native “hide all other windows" menu item. + /// + /// ## platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + HideOthers, + /// A native "Show all windows for this app" menu item. + /// + /// ## platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + ShowAll, + /// A native "Services" menu item. + /// + /// ## platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + Services, + /// A native "Close current window" menu item. + CloseWindow, + /// A native "Quit/// + Quit, + /// A native "Copy" menu item. + /// + /// ## Platform-specific: + /// + /// - **macOS**: macOS require this menu item to enable "Copy" keyboard shortcut for your app. + /// - **Linux Wayland**: Not implmeneted. + Copy, + /// A native "Cut" menu item. + /// + /// ## Platform-specific: + /// + /// - **macOS**: macOS require this menu item to enable "Cut" keyboard shortcut for your app. + /// - **Linux Wayland**: Not implmeneted. + Cut, + /// A native "Paste" menu item. + /// + /// ## Platform-specific: + /// + /// - **macOS**: macOS require this menu item to enable "Paste" keyboard shortcut for your app. + /// - **Linux Wayland**: Not implmeneted. + Paste, + /// A native "Undo" menu item. + /// + /// ## Platform-specific: + /// + /// - **macOS**: macOS require this menu item to enable "Undo" keyboard shortcut for your app. + /// - **Windows / Linux**: Unsupported. + Undo, + /// A native "Redo" menu item. + /// + /// ## Platform-specific: + /// + /// - **macOS**: macOS require this menu item to enable "Redo" keyboard shortcut for your app. + /// - **Windows / Linux**: Unsupported. + Redo, + /// A native "Select All" menu item. + /// + /// ## Platform-specific: + /// + /// - **macOS**: macOS require this menu item to enable "Select All" keyboard shortcut for your app. + /// - **Linux Wayland**: Not implmeneted. + SelectAll, + /// A native "Enter fullscreen" menu item. + /// + /// ## platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + EnterFullScreen, + /// A native "Minimize current window" menu item. + Minimize, + /// A native "Zoom" menu item. + /// + /// ## platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + Zoom, + /// Represends a Separator in the menu. + Separator, +} + +/// Application metadata for the [`NativeMenuItem::About`]. +/// +/// ## Platform-specific +/// +/// - **macOS**: The metadata is ignored. +#[derive(Debug, Clone, Default)] +pub struct AboutMetadata { + /// The application name. + pub version: Option, + /// The authors of the application. + pub authors: Option>, + /// Application comments. + pub comments: Option, + /// The copyright of the application. + pub copyright: Option, + /// The license of the application. + pub license: Option, + /// The application website. + pub website: Option, + /// The website label. + pub website_label: Option, +} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index ae58cb3..84c5e04 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -1,41 +1,50 @@ mod accelerator; -use crate::counter::Counter; +use crate::{counter::Counter, NativeMenuItem}; use accelerator::{to_gtk_accelerator, to_gtk_menemenoic}; use gtk::{prelude::*, Orientation}; use std::{cell::RefCell, collections::HashMap, rc::Rc}; static COUNTER: Counter = Counter::new(); -#[derive(PartialEq, Eq)] -enum MenuEntryType { - Submenu, - Text, -} - /// Generic shared type describing a menu entry. It can be one of [`MenuEntryType`] +#[derive(Debug, Default)] struct MenuEntry { label: String, enabled: bool, r#type: MenuEntryType, item_id: Option, accelerator: Option, - // NOTE(amrbashir): because gtk doesn't allow using the same `gtk::MenuItem` + native_menu_item: Option, + // NOTE(amrbashir): because gtk doesn't allow using the same [`gtk::MenuItem`] // multiple times, and thus can't be used in multiple windows, each entry - // keeps a vector of a `gtk::MenuItem` or a tuple of `gtk::MenuItem` and `gtk::Menu` - // and push to it every time `Menu::init_for_gtk_window` is called. - item_gtk_items: Option>>>, - menu_gtk_items: Option>>>, + // keeps a vector of a [`gtk::MenuItem`] or a tuple of [`gtk::MenuItem`] and [`gtk::Menu`] if its a menu + // and push to it every time [`Menu::init_for_gtk_window`] is called. + native_items: Option>>>, + native_menus: Option>>>, entries: Option>>>, } +#[derive(PartialEq, Eq, Debug)] +enum MenuEntryType { + Submenu, + Text, + Native, +} + +impl Default for MenuEntryType { + fn default() -> Self { + MenuEntryType::Text + } +} + struct InnerMenu { entries: Vec>>, - // NOTE(amrbashir): because gtk doesn't allow using the same `gtk::MenuBar` and `gtk::Box` - // multiple times, and thus can't be used in multiple windows, entry - // keeps a vector of a tuple of `gtk::MenuBar` and `gtk::Box` + // NOTE(amrbashir): because gtk doesn't allow using the same [`gtk::MenuBar`] and [`gtk::Box`] + // multiple times, and thus can't be used in multiple windows. each menu + // keeps a hashmap of window pointer as the key and a tuple of [`gtk::MenuBar`] and [`gtk::Box`] as the value // and push to it every time `Menu::init_for_gtk_window` is called. - gtk_items: HashMap, Rc)>, + native_menus: HashMap, Rc)>, accel_group: gtk::AccelGroup, } @@ -46,154 +55,117 @@ impl Menu { pub fn new() -> Self { Self(Rc::new(RefCell::new(InnerMenu { entries: Vec::new(), - gtk_items: HashMap::new(), + native_menus: HashMap::new(), accel_group: gtk::AccelGroup::new(), }))) } pub fn add_submenu(&mut self, label: impl AsRef, enabled: bool) -> Submenu { - let label = label.as_ref().to_string(); let entry = Rc::new(RefCell::new(MenuEntry { - label: label.clone(), + label: label.as_ref().to_string(), enabled, entries: Some(Vec::new()), r#type: MenuEntryType::Submenu, - item_id: None, - accelerator: None, - menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), - item_gtk_items: None, + native_menus: Some(Rc::new(RefCell::new(Vec::new()))), + ..Default::default() })); self.0.borrow_mut().entries.push(entry.clone()); Submenu(entry) } - pub fn init_for_gtk_window(&self, w: &W) -> Rc + pub fn init_for_gtk_window(&self, window: &W) -> Rc where + W: IsA, W: IsA, W: IsA, { let mut inner = self.0.borrow_mut(); - // This is the first time this method has been called on a window - if inner.gtk_items.get(&(w.as_ptr() as _)).is_none() { + // This is the first time this method has been called on this window + // so we need to create the menubar and its parent box + if inner.native_menus.get(&(window.as_ptr() as _)).is_none() { let menu_bar = gtk::MenuBar::new(); let vbox = gtk::Box::new(Orientation::Vertical, 0); - w.add(&vbox); + window.add(&vbox); inner - .gtk_items - .insert(w.as_ptr() as _, (Some(menu_bar), Rc::new(vbox))); + .native_menus + .insert(window.as_ptr() as _, (Some(menu_bar), Rc::new(vbox))); } - if let Some((menu_bar, vbox)) = inner.gtk_items.get(&(w.as_ptr() as _)) { + if let Some((menu_bar, vbox)) = inner.native_menus.get(&(window.as_ptr() as _)) { // This is NOT the first time this method has been called on a window. - // So it already contains a `gtk::Box` but it doesn't have a `gtk::MenuBar` - // because it was probably removed using `Menu::remove_for_gtk_window` + // So it already contains a [`gtk::Box`] but it doesn't have a [`gtk::MenuBar`] + // because it was probably removed using [`Menu::remove_for_gtk_window`] // so we only need to create the menubar if menu_bar.is_none() { let vbox = Rc::clone(vbox); inner - .gtk_items - .insert(w.as_ptr() as _, (Some(gtk::MenuBar::new()), vbox)); + .native_menus + .insert(window.as_ptr() as _, (Some(gtk::MenuBar::new()), vbox)); } } - let (menu_bar, vbox) = inner.gtk_items.get(&(w.as_ptr() as _)).unwrap(); - + // Construct the entries of the menubar + let (menu_bar, vbox) = inner.native_menus.get(&(window.as_ptr() as _)).unwrap(); add_entries_to_menu( menu_bar.as_ref().unwrap(), &inner.entries, &inner.accel_group, ); - w.add_accel_group(&inner.accel_group); + window.add_accel_group(&inner.accel_group); + // Show the menubar on the window vbox.pack_start(menu_bar.as_ref().unwrap(), false, false, 0); vbox.show_all(); Rc::clone(vbox) } - pub fn remove_for_gtk_window(&self, w: &W) + pub fn remove_for_gtk_window(&self, window: &W) where - W: IsA, + W: IsA, W: IsA, { let mut inner = self.0.borrow_mut(); - if let Some((menu_bar, vbox)) = inner.gtk_items.get(&(w.as_ptr() as _)) { - vbox.remove(menu_bar.as_ref().unwrap()); - w.remove_accel_group(&inner.accel_group); + if let Some((Some(menu_bar), vbox)) = inner.native_menus.get(&(window.as_ptr() as _)) { + // Remove the [`gtk::Menubar`] from the widget tree + unsafe { menu_bar.destroy() }; + // Detach the accelerators from the window + window.remove_accel_group(&inner.accel_group); + // Remove the removed [`gtk::Menubar`] from our cache let vbox = Rc::clone(vbox); - inner.gtk_items.insert(w.as_ptr() as _, (None, vbox)); + inner + .native_menus + .insert(window.as_ptr() as _, (None, vbox)); } } - pub fn hide_for_gtk_window(&self, w: &W) + pub fn hide_for_gtk_window(&self, window: &W) where - W: IsA, - W: IsA, + W: IsA, { - if let Some((menu_bar, _)) = self.0.borrow().gtk_items.get(&(w.as_ptr() as isize)) { - if let Some(menu_bar) = menu_bar { - menu_bar.hide(); - } + if let Some((Some(menu_bar), _)) = self + .0 + .borrow() + .native_menus + .get(&(window.as_ptr() as isize)) + { + menu_bar.hide(); } } - pub fn show_for_gtk_window(&self, w: &W) + pub fn show_for_gtk_window(&self, window: &W) where - W: IsA, - W: IsA, + W: IsA, { - if let Some((menu_bar, _)) = self.0.borrow().gtk_items.get(&(w.as_ptr() as isize)) { - if let Some(menu_bar) = menu_bar { - menu_bar.show_all(); - } - } - } -} - -fn add_entries_to_menu>( - gtk_menu: &M, - entries: &Vec>>, - accel_group: >k::AccelGroup, -) { - for entry in entries { - let mut entry = entry.borrow_mut(); - let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label)); - gtk_menu.append(>k_item); - gtk_item.set_sensitive(entry.enabled); - if entry.r#type == MenuEntryType::Submenu { - let gtk_menu = gtk::Menu::new(); - gtk_item.set_submenu(Some(>k_menu)); - add_entries_to_menu(>k_menu, entry.entries.as_ref().unwrap(), accel_group); - entry - .menu_gtk_items - .as_mut() - .unwrap() - .borrow_mut() - .push((gtk_item, gtk_menu)); - } else { - if let Some(accelerator) = &entry.accelerator { - let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); - gtk_item.add_accelerator( - "activate", - accel_group, - key, - modifiers, - gtk::AccelFlags::VISIBLE, - ); - } - - let id = entry.item_id.unwrap_or_default(); - gtk_item.connect_activate(move |_| { - let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); - }); - entry - .item_gtk_items - .as_mut() - .unwrap() - .borrow_mut() - .push(gtk_item); + if let Some((Some(menu_bar), _)) = self + .0 + .borrow() + .native_menus + .get(&(window.as_ptr() as isize)) + { + menu_bar.show_all(); } } } @@ -209,8 +181,8 @@ impl Submenu { pub fn set_label(&mut self, label: impl AsRef) { let label = label.as_ref().to_string(); let mut entry = self.0.borrow_mut(); - for (item, _) in entry.menu_gtk_items.as_ref().unwrap().borrow().iter() { - item.set_label(&label); + for (item, _) in entry.native_menus.as_ref().unwrap().borrow().iter() { + item.set_label(&to_gtk_menemenoic(&label)); } entry.label = label; } @@ -222,7 +194,7 @@ impl Submenu { pub fn set_enabled(&mut self, enabled: bool) { let mut entry = self.0.borrow_mut(); entry.enabled = true; - for (item, _) in entry.menu_gtk_items.as_ref().unwrap().borrow().iter() { + for (item, _) in entry.native_menus.as_ref().unwrap().borrow().iter() { item.set_sensitive(enabled); } } @@ -233,10 +205,8 @@ impl Submenu { enabled, entries: Some(Vec::new()), r#type: MenuEntryType::Submenu, - item_id: None, - accelerator: None, - menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), - item_gtk_items: None, + native_menus: Some(Rc::new(RefCell::new(Vec::new()))), + ..Default::default() })); self.0 .borrow_mut() @@ -254,14 +224,13 @@ impl Submenu { accelerator: Option<&str>, ) -> TextMenuItem { let entry = Rc::new(RefCell::new(MenuEntry { - label: to_gtk_menemenoic(label), + label: label.as_ref().to_string(), enabled, - entries: None, r#type: MenuEntryType::Text, item_id: Some(COUNTER.next()), accelerator: accelerator.map(|s| s.to_string()), - menu_gtk_items: None, - item_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))), + native_items: Some(Rc::new(RefCell::new(Vec::new()))), + ..Default::default() })); self.0 .borrow_mut() @@ -271,6 +240,15 @@ impl Submenu { .push(entry.clone()); TextMenuItem(entry) } + + pub fn add_native_item(&mut self, item: NativeMenuItem) { + let entry = Rc::new(RefCell::new(MenuEntry { + r#type: MenuEntryType::Native, + native_menu_item: Some(item), + ..Default::default() + })); + self.0.borrow_mut().entries.as_mut().unwrap().push(entry); + } } #[derive(Clone)] @@ -284,8 +262,8 @@ impl TextMenuItem { pub fn set_label(&mut self, label: impl AsRef) { let label = label.as_ref().to_string(); let mut entry = self.0.borrow_mut(); - for item in entry.item_gtk_items.as_ref().unwrap().borrow().iter() { - item.set_label(&label); + for item in entry.native_items.as_ref().unwrap().borrow().iter() { + item.set_label(&to_gtk_menemenoic(&label)); } entry.label = label; } @@ -296,7 +274,7 @@ impl TextMenuItem { pub fn set_enabled(&mut self, enabled: bool) { let mut entry = self.0.borrow_mut(); - for item in entry.item_gtk_items.as_ref().unwrap().borrow().iter() { + for item in entry.native_items.as_ref().unwrap().borrow().iter() { item.set_sensitive(enabled); } entry.enabled = enabled; @@ -306,3 +284,156 @@ impl TextMenuItem { self.0.borrow().item_id.unwrap() } } + +fn add_entries_to_menu( + gtk_menu: &M, + entries: &Vec>>, + accel_group: >k::AccelGroup, +) where + M: IsA, +{ + for entry in entries { + let mut entry = entry.borrow_mut(); + match entry.r#type { + MenuEntryType::Submenu => { + let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label)); + gtk_menu.append(>k_item); + gtk_item.set_sensitive(entry.enabled); + let gtk_menu = gtk::Menu::new(); + gtk_item.set_submenu(Some(>k_menu)); + add_entries_to_menu(>k_menu, entry.entries.as_ref().unwrap(), accel_group); + entry + .native_menus + .as_mut() + .unwrap() + .borrow_mut() + .push((gtk_item, gtk_menu)); + } + MenuEntryType::Text => { + let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label)); + gtk_menu.append(>k_item); + gtk_item.set_sensitive(entry.enabled); + if let Some(accelerator) = &entry.accelerator { + let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); + gtk_item.add_accelerator( + "activate", + accel_group, + key, + modifiers, + gtk::AccelFlags::VISIBLE, + ); + } + + let id = entry.item_id.unwrap(); + gtk_item.connect_activate(move |_| { + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); + }); + entry + .native_items + .as_mut() + .unwrap() + .borrow_mut() + .push(gtk_item); + } + MenuEntryType::Native => match entry.native_menu_item.as_ref().unwrap() { + NativeMenuItem::Copy => { + let gtk_item = gtk::MenuItem::with_mnemonic("_Copy"); + let (key, modifiers) = gtk::accelerator_parse("X"); + gtk_item + .child() + .unwrap() + .downcast::() + .unwrap() + .set_accel(key, modifiers); + gtk_item.connect_activate(move |_| { + // TODO: wayland + if let Ok(xdo) = libxdo::XDo::new(None) { + let _ = xdo.send_keysequence("ctrl+c", 0); + } + }); + gtk_menu.append(>k_item); + } + NativeMenuItem::Cut => { + let gtk_item = gtk::MenuItem::with_mnemonic("Cu_t"); + let (key, modifiers) = gtk::accelerator_parse("X"); + gtk_item + .child() + .unwrap() + .downcast::() + .unwrap() + .set_accel(key, modifiers); + gtk_item.connect_activate(move |_| { + // TODO: wayland + if let Ok(xdo) = libxdo::XDo::new(None) { + let _ = xdo.send_keysequence("ctrl+x", 0); + } + }); + gtk_menu.append(>k_item); + } + NativeMenuItem::Paste => { + let gtk_item = gtk::MenuItem::with_mnemonic("_Paste"); + let (key, modifiers) = gtk::accelerator_parse("V"); + gtk_item + .child() + .unwrap() + .downcast::() + .unwrap() + .set_accel(key, modifiers); + gtk_item.connect_activate(move |_| { + // TODO: wayland + if let Ok(xdo) = libxdo::XDo::new(None) { + let _ = xdo.send_keysequence("ctrl+v", 0); + } + }); + gtk_menu.append(>k_item); + } + NativeMenuItem::SelectAll => { + let gtk_item = gtk::MenuItem::with_mnemonic("Select _All"); + let (key, modifiers) = gtk::accelerator_parse("A"); + gtk_item + .child() + .unwrap() + .downcast::() + .unwrap() + .set_accel(key, modifiers); + gtk_item.connect_activate(move |_| { + // TODO: wayland + if let Ok(xdo) = libxdo::XDo::new(None) { + let _ = xdo.send_keysequence("ctrl+a", 0); + } + }); + gtk_menu.append(>k_item); + } + NativeMenuItem::Separator => { + gtk_menu.append(>k::SeparatorMenuItem::new()); + } + NativeMenuItem::Minimize => { + let gtk_item = gtk::MenuItem::with_mnemonic("_Minimize"); + gtk_item.connect_activate(move |m| { + if let Some(window) = m.window() { + window.iconify() + } + }); + gtk_menu.append(>k_item); + } + NativeMenuItem::CloseWindow => { + let gtk_item = gtk::MenuItem::with_mnemonic("C_lose Window"); + gtk_item.connect_activate(move |m| { + if let Some(window) = m.window() { + window.destroy() + } + }); + gtk_menu.append(>k_item); + } + NativeMenuItem::Quit => { + let gtk_item = gtk::MenuItem::with_mnemonic("_Quit"); + gtk_item.connect_activate(move |_| { + std::process::exit(0); + }); + gtk_menu.append(>k_item); + } + _ => {} + }, + } + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 5189cbd..7258547 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -3,17 +3,19 @@ mod accelerator; mod util; -use crate::counter::Counter; +use crate::{counter::Counter, NativeMenuItem}; use std::{cell::RefCell, rc::Rc}; use util::{decode_wide, encode_wide, LOWORD}; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, WPARAM}, UI::{ + Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VK_CONTROL}, Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass}, WindowsAndMessaging::{ - AppendMenuW, CreateAcceleratorTableW, CreateMenu, DrawMenuBar, EnableMenuItem, - GetMenuItemInfoW, SetMenu, SetMenuItemInfoW, ACCEL, HACCEL, HMENU, MENUITEMINFOW, - MFS_DISABLED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MIIM_STATE, MIIM_STRING, + AppendMenuW, CloseWindow, CreateAcceleratorTableW, CreateMenu, DrawMenuBar, + EnableMenuItem, GetMenuItemInfoW, PostQuitMessage, SetMenu, SetMenuItemInfoW, + ShowWindow, ACCEL, HACCEL, HMENU, MENUITEMINFOW, MFS_DISABLED, MF_DISABLED, MF_ENABLED, + MF_GRAYED, MF_POPUP, MF_SEPARATOR, MF_STRING, MIIM_STATE, MIIM_STRING, SW_MINIMIZE, WM_COMMAND, }, }, @@ -21,6 +23,7 @@ use windows_sys::Win32::{ use self::accelerator::parse_accelerator; +const COUNTER_START: u64 = 563; static COUNTER: Counter = Counter::new_with_start(563); const MENU_SUBCLASS_ID: usize = 232; @@ -189,7 +192,7 @@ impl Submenu { accelerator: Option<&str>, ) -> TextMenuItem { let id = COUNTER.next(); - let mut flags = MF_POPUP; + let mut flags = MF_STRING; if !enabled { flags |= MF_GRAYED; } @@ -218,6 +221,28 @@ impl Submenu { parent_hmenu: self.hmenu, } } + + pub fn add_native_item(&mut self, item: NativeMenuItem) { + let (label, flags) = match item { + NativeMenuItem::Copy => ("&Copy\tCtrl+C", MF_STRING), + NativeMenuItem::Cut => ("Cu&t\tCtrl+X", MF_STRING), + NativeMenuItem::Paste => ("&Paste\tCtrl+V", MF_STRING), + NativeMenuItem::SelectAll => ("Select&All", MF_STRING), + NativeMenuItem::Separator => ("", MF_SEPARATOR), + NativeMenuItem::Minimize => ("&Minimize", MF_STRING), + NativeMenuItem::CloseWindow => ("Close", MF_STRING), + NativeMenuItem::Quit => ("Exit", MF_STRING), + _ => return, + }; + unsafe { + AppendMenuW( + self.hmenu, + flags, + item.id() as _, + encode_wide(label).as_ptr(), + ) + }; + } } #[derive(Clone)] @@ -302,10 +327,107 @@ unsafe extern "system" fn menu_subclass_proc( _uidsubclass: usize, _dwrefdata: usize, ) -> LRESULT { - let id = LOWORD(wparam as _); - if msg == WM_COMMAND && 0 < id && (id as u64) < COUNTER.current() { - let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id: id as _ }); + let mut ret = -1; + if msg == WM_COMMAND { + let id = LOWORD(wparam as _) as u64; + + // Custom menu items + if COUNTER_START < id && id < COUNTER.current() { + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); + ret = 0; + }; + + // Native menu items + if NativeMenuItem::is_id_of_native(id) { + ret = 0; + match id { + _ if id == NativeMenuItem::Copy.id() => { + execute_edit_command(EditCommand::Copy); + } + _ if id == NativeMenuItem::Cut.id() => { + execute_edit_command(EditCommand::Cut); + } + _ if id == NativeMenuItem::Paste.id() => { + execute_edit_command(EditCommand::Paste); + } + _ if id == NativeMenuItem::SelectAll.id() => { + execute_edit_command(EditCommand::SelectAll); + } + _ if id == NativeMenuItem::Minimize.id() => { + ShowWindow(hwnd, SW_MINIMIZE); + } + _ if id == NativeMenuItem::CloseWindow.id() => { + CloseWindow(hwnd); + } + _ if id == NativeMenuItem::Quit.id() => { + PostQuitMessage(0); + } + _ => unreachable!(), + } + } + } + + if ret == -1 { + DefSubclassProc(hwnd, msg, wparam, lparam) + } else { + ret + } +} + +enum EditCommand { + Copy, + Cut, + Paste, + SelectAll, +} + +fn execute_edit_command(command: EditCommand) { + let key = match command { + EditCommand::Copy => 0x43, // c + EditCommand::Cut => 0x58, // x + EditCommand::Paste => 0x56, // v + EditCommand::SelectAll => 0x41, // a }; - DefSubclassProc(hwnd, msg, wparam, lparam) + unsafe { + let mut inputs: [INPUT; 4] = std::mem::zeroed(); + inputs[0].r#type = INPUT_KEYBOARD; + inputs[0].Anonymous.ki.wVk = VK_CONTROL; + inputs[2].Anonymous.ki.dwFlags = 0; + + inputs[1].r#type = INPUT_KEYBOARD; + inputs[1].Anonymous.ki.wVk = key; + inputs[2].Anonymous.ki.dwFlags = 0; + + inputs[2].r#type = INPUT_KEYBOARD; + inputs[2].Anonymous.ki.wVk = key; + inputs[2].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP; + + inputs[3].r#type = INPUT_KEYBOARD; + inputs[3].Anonymous.ki.wVk = VK_CONTROL; + inputs[3].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP; + + let ret = SendInput(4, &inputs as *const _, std::mem::size_of::() as _); + dbg!(ret); + } +} + +impl NativeMenuItem { + fn id(&self) -> u64 { + match self { + NativeMenuItem::Copy => 301, + NativeMenuItem::Cut => 302, + NativeMenuItem::Paste => 303, + NativeMenuItem::SelectAll => 304, + NativeMenuItem::Separator => 305, + NativeMenuItem::Minimize => 306, + NativeMenuItem::CloseWindow => 307, + NativeMenuItem::Quit => 308, + _ => unreachable!(), + } + } + + fn is_id_of_native(id: u64) -> bool { + (301..=308).contains(&id) + } }