From 812ff0d37a28685ad9fd1a278a0a01256e1958ae Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Wed, 23 Nov 2022 18:29:52 +0200 Subject: [PATCH] refactor: rewrite (#18) * refactor: rewrite * fix syncing check items and cleanup * clippy * Add `append`, `prepend` and `insert` * accept different menu items in `*_list` methods * add context menu for gtk * add `with_items` * add `items` getter * chore: unreachable! and typos * implement remove * `*_list` -> `*_items` * fix winit example * add `show_context_menu_for_gtk_window` on `Submenu` type * Add windows implementation * TextMenuItem -> MenuItem, MenuItem trait -> MenuEntry * Add `PredfinedMenuItem` * move internal mod into its own file * update tao example to latest tao's `muda` branch * fix build on linux with latest tao changes * Fix accelerators on Linux * update examples * remove recursive removal of submenus * remvoe gtk menu items recursively * fix tao example on macos * On Windows, remove parents hmenu when removing an item * Add documentation * update README.md * use insert_items with postion 0 for prepend_items * Add menu mnemonics in examples * Add `ContextMenu` trait * Add methods to `ContextMenu` trait necessary for tray icon * fix linux build * fix context menu on gtk * Expose gtk::Menu in ContextMenu trait * Revert context menu to create a gtk::Menu on each call * clippy lints * cleanup crate structure * update docs * Fix doc tests and links * more docs fixes * error handling * macOS implementation (#19) * partial macOS implementation * fix context menu examples * add accelerator support for macOS * strip ampersands from titles on macOS * add CMD_OR_CTRL shorthand for modifiers * implement actions for predefined menu items on macos * fix examples * more predefined items * implement insert for macos * refactor macOS implementation * menu state getters and setters on macOS * implement remove for macOS * code tweaks * add show_context_menu_for_nsview for Submenu on macOS * docs improvements * allow adding item to the same menu multiple times on macOS * implement `items` for macOS * strip only single ampersands from menu titles * add support for menu item actions on macOS * add app name to macOS About, Hide, Quit menu items * add methods to set app window and help menus on macOS * fix clickable submenu titles on macOS * refactor submenu for safe reuse on macOS * fmt & clippy * few cleanups * fix docs * clippy * fix docs * cleanup examples * fix tests * fix clippy?? * use cargo action instead * ??? * Replace popUpContextMenu with popUpMenuPositioningItem Co-authored-by: Caesar Schinas Co-authored-by: Wu Wayne --- .github/workflows/clippy-fmt.yml | 9 +- .gitignore | 1 + Cargo.toml | 11 +- README.md | 77 +- examples/tao.rs | 183 +- examples/winit.rs | 162 +- src/accelerator.rs | 136 +- src/check_menu_item.rs | 80 + src/counter.rs | 23 - src/error.rs | 22 + src/lib.rs | 640 ++----- src/menu.rs | 261 +++ src/menu_item.rs | 62 + .../{linux => gtk}/accelerator.rs | 42 +- src/platform_impl/gtk/mod.rs | 1691 +++++++++++++++++ src/platform_impl/linux/mod.rs | 690 ------- src/platform_impl/macos/accelerator.rs | 15 +- src/platform_impl/macos/menu_item.rs | 271 --- src/platform_impl/macos/mod.rs | 988 ++++++++-- src/platform_impl/macos/util.rs | 41 + src/platform_impl/mod.rs | 12 +- src/platform_impl/windows/mod.rs | 1290 +++++++++---- src/platform_impl/windows/util.rs | 36 +- src/predefined.rs | 244 +++ src/submenu.rs | 168 ++ src/util.rs | 25 + 26 files changed, 4949 insertions(+), 2231 deletions(-) create mode 100644 src/check_menu_item.rs delete mode 100644 src/counter.rs create mode 100644 src/error.rs create mode 100644 src/menu.rs create mode 100644 src/menu_item.rs rename src/platform_impl/{linux => gtk}/accelerator.rs (85%) create mode 100644 src/platform_impl/gtk/mod.rs delete mode 100644 src/platform_impl/linux/mod.rs delete mode 100644 src/platform_impl/macos/menu_item.rs create mode 100644 src/platform_impl/macos/util.rs create mode 100644 src/predefined.rs create mode 100644 src/submenu.rs create mode 100644 src/util.rs diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index c5c47e2..0ee736f 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -19,6 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: install system deps + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libxdo-dev libayatana-appindicator3-dev - name: install stable uses: actions-rs/toolchain@v1 with: @@ -27,10 +31,9 @@ jobs: override: true components: clippy - - name: clippy - uses: actions-rs/clippy-check@v1 + - uses: actions-rs/cargo@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} + command: clippy args: --all-targets --all-features -- -D warnings fmt: diff --git a/.gitignore b/.gitignore index 96ef6c0..6bfa6c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +/.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 2898738..c4d9425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,23 +12,24 @@ categories = ["gui"] [dependencies] crossbeam-channel = "0.5" -once_cell = "1.10" keyboard-types = "0.6" +once_cell = "1" +thiserror = "1.0.37" [target.'cfg(target_os = "windows")'.dependencies.windows-sys] -version = "0.34" +version = "0.42" features = [ "Win32_UI_WindowsAndMessaging", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_UI_Shell", "Win32_Globalization", - "Win32_UI_Input_KeyboardAndMouse" + "Win32_UI_Input_KeyboardAndMouse", ] [target.'cfg(target_os = "linux")'.dependencies] gdk = "0.15" -gtk = "0.15" +gtk = { version = "0.15", features = ["v3_22"] } libxdo = "0.6.0" [target.'cfg(target_os = "macos")'.dependencies] @@ -36,5 +37,5 @@ cocoa = "0.24" objc = "0.2" [dev-dependencies] -winit = { git = "https://github.com/rust-windowing/winit" } +winit = "0.27" tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } diff --git a/README.md b/README.md index 30ddbc4..573f4e7 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,32 @@ -# muda - -Menu Utilities for Desktop Applications. +muda is a Menu Utilities library for Desktop Applications. ## Example -Create the root menu and add submenus and men items. +Create the menu and add your items + ```rs -let mut menu = Menu::new(); +let menu = Menu::new(); +let menu_item2 = MenuItem::new("Menu item #2", false, None); +let submenu = Submenu::with_items("Submenu Outer", true,&[ + &MenuItem::new("Menu item #1", true, Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD))), + &PredefinedMenuItem::separator(), + &menu_item2, + &MenuItem::new("Menu item #3", true, None), + &PredefinedMenuItem::separator(), + &Submenu::with_items("Submenu Inner", true,&[ + &MenuItem::new("Submenu item #1", true, None), + &PredefinedMenuItem::separator(), + &menu_item2, + ]) +]); -let file_menu = menu.add_submenu("File", true); -let open_item = file_menu.add_text_item("Open", true); -let save_item = file_menu.add_text_item("Save", true); +``` -let edit_menu = menu.add_submenu("Edit", true); -let copy_item = file_menu.add_text_item("Copy", true); -let cut_item = file_menu.add_text_item("Cut", true); +Then Add your root menu to a Window on Windows and Linux Only or use it +as your global app menu on macOS +```rs +// --snip-- #[cfg(target_os = "windows")] menu.init_for_hwnd(window.hwnd() as isize); #[cfg(target_os = "linux")] @@ -23,7 +34,26 @@ menu.init_for_gtk_window(>k_window); #[cfg(target_os = "macos")] menu.init_for_nsapp(); ``` -Then listen for the events + +## Context menus (Popup menus) + +You can also use a [`Menu`] or a [`Submenu`] show a context menu. + +```rs +// --snip-- +let x = 100; +let y = 120; +#[cfg(target_os = "windows")] +menu.show_context_menu_for_hwnd(window.hwnd() as isize, x, y); +#[cfg(target_os = "linux")] +menu.show_context_menu_for_gtk_window(>k_window, x, y); +#[cfg(target_os = "macos")] +menu.show_context_menu_for_nsview(nsview, x, y); +``` +## Processing menu events + +You can use [`menu_event_receiver`](https://docs.rs/muda/latest/muda/fn.menu_event_receiver.html) to get a reference to the [`MenuEventReceiver`](https://docs.rs/muda/latest/muda/type.MenuEventReceiver.html) +which you can use to listen to events when a menu item is activated ```rs if let Ok(event) = menu_event_receiver().try_recv() { match event.id { @@ -34,3 +64,26 @@ if let Ok(event) = menu_event_receiver().try_recv() { } } ``` + +## Platform-specific notes: + +### Accelerators on Windows + +Accelerators don't work unless the win32 message loop calls +[`TranslateAcceleratorW`](https://docs.rs/windows-sys/latest/windows_sys/Win32/UI/WindowsAndMessaging/fn.TranslateAcceleratorW.html) + +See [`Menu::init_for_hwnd`](https://docs.rs/muda/latest/muda/struct.Menu.html#method.init_for_hwnd) for more details + +### Linux + +`libx` is used to make the predfined `Copy`, `Cut`, `Paste` and `SelectAll` menu items work. Be sure to install following packages before building: + +Arch Linux / Manjaro: +```sh +pacman -S xdotool +``` + +Debian / Ubuntu: +```sh +sudo apt install libxdo-dev +``` diff --git a/examples/tao.rs b/examples/tao.rs index c0840e9..1ffec70 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -1,42 +1,119 @@ -use keyboard_types::Code; +#![allow(unused)] use muda::{ - accelerator::{Accelerator, Mods}, - menu_event_receiver, Menu, NativeMenuItem, + accelerator::{Accelerator, Code, Modifiers}, + menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem, + PredefinedMenuItem, Submenu, }; +#[cfg(target_os = "macos")] +use tao::platform::macos::WindowExtMacOS; #[cfg(target_os = "linux")] use tao::platform::unix::WindowExtUnix; #[cfg(target_os = "windows")] -use tao::platform::windows::WindowExtWindows; +use tao::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows}; use tao::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, + event::{ElementState, Event, MouseButton, WindowEvent}, + event_loop::{ControlFlow, EventLoopBuilder}, window::WindowBuilder, }; fn main() { - let event_loop = EventLoop::new(); + let mut event_loop_builder = EventLoopBuilder::new(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - let window2 = WindowBuilder::new().build(&event_loop).unwrap(); + let menu_bar = Menu::new(); - let mut menu_bar = Menu::new(); + #[cfg(target_os = "windows")] + { + let menu_bar_c = menu_bar.clone(); + event_loop_builder.with_msg_hook(move |msg| { + use windows_sys::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, MSG}; + unsafe { + let msg = msg as *const MSG; + let translated = TranslateAcceleratorW((*msg).hwnd, menu_bar_c.haccel(), msg); + translated == 1 + } + }); + } - let mut file_menu = menu_bar.add_submenu("&File", true); - let mut open_item = file_menu.add_item("&Open", true, None); - let mut save_item = file_menu.add_item( - "&Save", + let event_loop = event_loop_builder.build(); + + let window = WindowBuilder::new() + .with_title("Window 1") + .build(&event_loop) + .unwrap(); + let window2 = WindowBuilder::new() + .with_title("Window 2") + .build(&event_loop) + .unwrap(); + + #[cfg(target_os = "macos")] + { + let app_m = Submenu::new("App", true); + menu_bar.append(&app_m); + app_m.append_items(&[ + &PredefinedMenuItem::about(None, None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::services(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::hide(None), + &PredefinedMenuItem::hide_others(None), + &PredefinedMenuItem::show_all(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::quit(None), + ]); + } + + let file_m = Submenu::new("&File", true); + let edit_m = Submenu::new("&Edit", true); + let window_m = Submenu::new("&Window", true); + + menu_bar.append_items(&[&file_m, &edit_m, &window_m]); + + let custom_i_1 = MenuItem::new( + "C&ustom 1", true, - Some(Accelerator::new(Mods::Ctrl, Code::KeyS)), + Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), + ); + let custom_i_2 = MenuItem::new("Custom 2", false, None); + let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None); + let check_custom_i_2 = CheckMenuItem::new("Check Custom 2", false, true, None); + let check_custom_i_3 = CheckMenuItem::new( + "Check Custom 3", + true, + true, + Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)), ); - file_menu.add_native_item(NativeMenuItem::Minimize); - file_menu.add_native_item(NativeMenuItem::CloseWindow); - file_menu.add_native_item(NativeMenuItem::Quit); - 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); + let copy_i = PredefinedMenuItem::copy(None); + let cut_i = PredefinedMenuItem::cut(None); + let paste_i = PredefinedMenuItem::paste(None); + + file_m.append_items(&[ + &custom_i_1, + &custom_i_2, + &window_m, + &PredefinedMenuItem::separator(), + &check_custom_i_1, + ]); + + window_m.append_items(&[ + &PredefinedMenuItem::minimize(None), + &PredefinedMenuItem::maximize(None), + &PredefinedMenuItem::close_window(Some("Close")), + &PredefinedMenuItem::fullscreen(None), + &PredefinedMenuItem::about( + None, + Some(AboutMetadata { + name: Some("tao".to_string()), + copyright: Some("Copyright tao".to_string()), + ..Default::default() + }), + ), + &check_custom_i_3, + &custom_i_2, + &custom_i_1, + ]); + + edit_m.append_items(&[©_i, &paste_i, &PredefinedMenuItem::separator()]); #[cfg(target_os = "windows")] { @@ -48,44 +125,64 @@ fn main() { menu_bar.init_for_gtk_window(window.gtk_window()); menu_bar.init_for_gtk_window(window2.gtk_window()); } + #[cfg(target_os = "macos")] + { + menu_bar.init_for_nsapp(); + window_m.set_windows_menu_for_nsapp(); + } let menu_channel = menu_event_receiver(); - let mut open_item_disabled = false; - let mut counter = 0; + let mut x = 0_f64; + let mut y = 0_f64; 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::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + window_id, + .. + } => { + if window_id == window.id() { + x = position.x; + y = position.y; + } + } + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Right, + .. + }, + window_id, + .. + } => { + if window_id == window2.id() { + #[cfg(target_os = "windows")] + window_m.show_context_menu_for_hwnd(window2.hwnd() as _, x, y); + #[cfg(target_os = "linux")] + window_m.show_context_menu_for_gtk_window(window2.gtk_window(), x, y); + #[cfg(target_os = "macos")] + menu_bar.show_context_menu_for_nsview(window2.ns_view() as _, x, y); + } + } Event::MainEventsCleared => { - // window.request_redraw(); + window.request_redraw(); } _ => (), } if let Ok(event) = menu_channel.try_recv() { - match event.id { - _ if event.id == save_item.id() => { - println!("Save menu item activated!"); - counter += 1; - save_item.set_label(format!("&Save activated {counter} times")); - - if !open_item_disabled { - println!("Open item disabled!"); - open_item.set_enabled(false); - open_item_disabled = true; - } - } - _ => {} + if event.id == custom_i_1.id() { + file_m.insert(&MenuItem::new("New Menu Item", false, None), 2); } + println!("{:?}", event); } }) } diff --git a/examples/winit.rs b/examples/winit.rs index 24ef5c9..85839d8 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,14 +1,15 @@ -use keyboard_types::Code; +#![allow(unused)] use muda::{ - accelerator::{Accelerator, Mods}, - menu_event_receiver, Menu, NativeMenuItem, + accelerator::{Accelerator, Code, Modifiers}, + menu_event_receiver, AboutMetadata, CheckMenuItem, ContextMenu, Menu, MenuItem, + PredefinedMenuItem, Submenu, }; #[cfg(target_os = "macos")] -use winit::platform::macos::EventLoopBuilderExtMacOS; +use winit::platform::macos::{EventLoopBuilderExtMacOS, WindowExtMacOS}; #[cfg(target_os = "windows")] use winit::platform::windows::{EventLoopBuilderExtWindows, WindowExtWindows}; use winit::{ - event::{Event, WindowEvent}, + event::{ElementState, Event, MouseButton, WindowEvent}, event_loop::{ControlFlow, EventLoopBuilder}, window::WindowBuilder, }; @@ -16,7 +17,7 @@ use winit::{ fn main() { let mut event_loop_builder = EventLoopBuilder::new(); - let mut menu_bar = Menu::new(); + let menu_bar = Menu::new(); #[cfg(target_os = "windows")] { @@ -24,7 +25,7 @@ fn main() { event_loop_builder.with_msg_hook(move |msg| { use windows_sys::Win32::UI::WindowsAndMessaging::{TranslateAcceleratorW, MSG}; unsafe { - let msg = msg as *mut MSG; + let msg = msg as *const MSG; let translated = TranslateAcceleratorW((*msg).hwnd, menu_bar_c.haccel(), msg); translated == 1 } @@ -33,56 +34,137 @@ fn main() { #[cfg(target_os = "macos")] event_loop_builder.with_default_menu(false); - #[allow(unused_mut)] - let mut event_loop = event_loop_builder.build(); + let event_loop = event_loop_builder.build(); - let window = WindowBuilder::new().build(&event_loop).unwrap(); - let _window2 = WindowBuilder::new().build(&event_loop).unwrap(); + let window = WindowBuilder::new() + .with_title("Window 1") + .build(&event_loop) + .unwrap(); + let window2 = WindowBuilder::new() + .with_title("Window 2") + .build(&event_loop) + .unwrap(); - let mut file_menu = menu_bar.add_submenu("&File", true); - let mut open_item = file_menu.add_item("&Open", true, None); - let mut save_item = file_menu.add_item( - "&Save", + #[cfg(target_os = "macos")] + { + let app_m = Submenu::new("App", true); + menu_bar.append(&app_m); + app_m.append_items(&[ + &PredefinedMenuItem::about(None, None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::services(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::hide(None), + &PredefinedMenuItem::hide_others(None), + &PredefinedMenuItem::show_all(None), + &PredefinedMenuItem::separator(), + &PredefinedMenuItem::quit(None), + ]); + } + + let file_m = Submenu::new("&File", true); + let edit_m = Submenu::new("&Edit", true); + let window_m = Submenu::new("&Window", true); + + menu_bar.append_items(&[&file_m, &edit_m, &window_m]); + + let custom_i_1 = MenuItem::new( + "C&ustom 1", true, - Some(Accelerator::new(Mods::Ctrl, Code::KeyS)), + Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), + ); + let custom_i_2 = MenuItem::new("Custom 2", false, None); + let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None); + let check_custom_i_2 = CheckMenuItem::new("Check Custom 2", false, true, None); + let check_custom_i_3 = CheckMenuItem::new( + "Check Custom 3", + true, + true, + Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)), ); - file_menu.add_native_item(NativeMenuItem::Minimize); - file_menu.add_native_item(NativeMenuItem::CloseWindow); - file_menu.add_native_item(NativeMenuItem::Quit); - 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); + let copy_i = PredefinedMenuItem::copy(None); + let cut_i = PredefinedMenuItem::cut(None); + let paste_i = PredefinedMenuItem::paste(None); + + file_m.append_items(&[ + &custom_i_1, + &custom_i_2, + &window_m, + &PredefinedMenuItem::separator(), + &check_custom_i_1, + ]); + + window_m.append_items(&[ + &PredefinedMenuItem::minimize(None), + &PredefinedMenuItem::maximize(None), + &PredefinedMenuItem::close_window(Some("Close")), + &PredefinedMenuItem::fullscreen(None), + &PredefinedMenuItem::about( + None, + Some(AboutMetadata { + name: Some("winit".to_string()), + copyright: Some("Copyright winit".to_string()), + ..Default::default() + }), + ), + &check_custom_i_3, + &custom_i_2, + &custom_i_1, + ]); + + edit_m.append_items(&[©_i, &paste_i, &PredefinedMenuItem::separator()]); #[cfg(target_os = "windows")] { menu_bar.init_for_hwnd(window.hwnd() as _); - menu_bar.init_for_hwnd(_window2.hwnd() as _); + menu_bar.init_for_hwnd(window2.hwnd() as _); } - #[cfg(target_os = "macos")] { menu_bar.init_for_nsapp(); + window_m.set_windows_menu_for_nsapp(); } let menu_channel = menu_event_receiver(); - let mut open_item_disabled = false; - let mut counter = 0; + let mut x = 0_f64; + let mut y = 0_f64; 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::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + window_id, + .. + } => { + if window_id == window.id() { + x = position.x; + y = position.y; + } + } + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state: ElementState::Pressed, + button: MouseButton::Right, + .. + }, + window_id, + .. + } => { + if window_id == window2.id() { + #[cfg(target_os = "windows")] + window_m.show_context_menu_for_hwnd(window2.hwnd(), x, y); + #[cfg(target_os = "macos")] + menu_bar.show_context_menu_for_nsview(window2.ns_view() as _, x, y); + } + } Event::MainEventsCleared => { window.request_redraw(); } @@ -90,20 +172,10 @@ fn main() { } if let Ok(event) = menu_channel.try_recv() { - match event.id { - _ if event.id == save_item.id() => { - println!("Save menu item activated!"); - counter += 1; - save_item.set_label(format!("&Save activated {counter} times")); - - if !open_item_disabled { - println!("Open item disabled!"); - open_item.set_enabled(false); - open_item_disabled = true; - } - } - _ => {} + if event.id == custom_i_1.id() { + file_m.insert(&MenuItem::new("New Menu Item", false, None), 2); } + println!("{:?}", event); } }) } diff --git a/src/accelerator.rs b/src/accelerator.rs index 034a1c9..40cc94c 100644 --- a/src/accelerator.rs +++ b/src/accelerator.rs @@ -1,28 +1,22 @@ -//! Accelerators describe keyboard shortcuts defined by the application. +//! Accelerators describe keyboard shortcuts for menu items. //! //! [`Accelerator`s](crate::accelerator::Accelerator) are used to define a keyboard shortcut consisting -//! of an optional combination of modifier keys (provided by [`SysMods`](crate::accelerator::SysMods), -//! [`RawMods`](crate::accelerator::RawMods) or [`Modifiers`](crate::accelerator::Modifiers)) and +//! of an optional combination of modifier keys (provided by [`Modifiers`](crate::accelerator::Modifiers)) and //! one key ([`Code`](crate::accelerator::Code)). //! //! # Examples //! They can be created directly -//! ``` -//! # use muda::accelerator::{Accelerator, Mods, Modifiers, Code}; -//! # -//! let accelerator = Accelerator::new(Mods::Shift, Code::KeyQ); -//! let accelerator_with_raw_mods = Accelerator::new(Mods::Shift, Code::KeyQ); +//! ```no_run +//! # use muda::accelerator::{Accelerator, Modifiers, Code}; +//! let accelerator = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyQ); //! let accelerator_without_mods = Accelerator::new(None, Code::KeyQ); -//! # assert_eq!(accelerator, accelerator_with_raw_mods); //! ``` //! or from `&str`, note that all modifiers //! have to be listed before the non-modifier key, `shift+alt+KeyQ` is legal, //! whereas `shift+q+alt` is not. -//! ``` -//! # use muda::accelerator::{Accelerator, Mods}; -//! # +//! ```no_run +//! # use muda::accelerator::{Accelerator}; //! let accelerator: Accelerator = "shift+alt+KeyQ".parse().unwrap(); -//! # //! # // This assert exists to ensure a test breaks once the //! # // statement above about ordering is no longer valid. //! # assert!("shift+KeyQ+alt".parse::().is_err()); @@ -32,8 +26,10 @@ pub use keyboard_types::{Code, Modifiers}; use std::{borrow::Borrow, hash::Hash, str::FromStr}; -/// Base `Accelerator` functions. -#[derive(Debug, Clone, PartialEq, Hash)] +/// A keyboard shortcut that consists of an optional combination +/// of modifier keys (provided by [`Modifiers`](crate::accelerator::Modifiers)) and +/// one key ([`Code`](crate::accelerator::Code)). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Accelerator { pub(crate) mods: Modifiers, pub(crate) key: Code, @@ -41,20 +37,22 @@ pub struct Accelerator { impl Accelerator { /// Creates a new accelerator to define keyboard shortcuts throughout your application. - pub fn new(mods: impl Into>, key: Code) -> Self { + /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::META`]/[`Modifiers::SUPER`] + pub fn new(mods: Option, key: Code) -> Self { Self { - mods: mods.into().unwrap_or_else(Modifiers::empty), + mods: mods.unwrap_or_else(Modifiers::empty), key, } } /// Returns `true` if this [`Code`] and [`Modifiers`] matches this `Accelerator`. - /// - /// [`Code`]: Code - /// [`Modifiers`]: crate::accelerator::Modifiers pub fn matches(&self, modifiers: impl Borrow, key: impl Borrow) -> bool { // Should be a const but const bit_or doesn't work here. - let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER; + let base_mods = Modifiers::SHIFT + | Modifiers::CONTROL + | Modifiers::ALT + | Modifiers::META + | Modifiers::SUPER; let modifiers = modifiers.borrow(); let key = key.borrow(); self.mods == *modifiers & base_mods && self.key == *key @@ -65,96 +63,20 @@ impl Accelerator { // compatible with tauri and it also open the option // to generate accelerator from string impl FromStr for Accelerator { - type Err = AcceleratorParseError; + type Err = crate::Error; fn from_str(accelerator_string: &str) -> Result { parse_accelerator(accelerator_string) } } -/// Represents the active modifier keys. -/// -/// This is intended to be clearer than [`Modifiers`], when describing accelerators. -/// -#[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Hash)] -pub enum Mods { - None, - Alt, - Ctrl, - Command, - CommandOrCtrl, - Meta, - Shift, - AltCtrl, - AltMeta, - AltShift, - CtrlShift, - CtrlMeta, - MetaShift, - AltCtrlMeta, - AltCtrlShift, - AltMetaShift, - CtrlMetaShift, - AltCtrlMetaShift, -} - -impl From for Option { - fn from(src: Mods) -> Option { - Some(src.into()) - } -} - -impl From for Modifiers { - fn from(src: Mods) -> Modifiers { - let (alt, ctrl, meta, shift) = match src { - Mods::None => (false, false, false, false), - Mods::Alt => (true, false, false, false), - Mods::Ctrl => (false, true, false, false), - Mods::Command => (false, false, true, false), - #[cfg(target_os = "macos")] - Mods::CommandOrCtrl => (false, false, true, false), - #[cfg(not(target_os = "macos"))] - Mods::CommandOrCtrl => (false, true, false, false), - Mods::Meta => (false, false, true, false), - Mods::Shift => (false, false, false, true), - Mods::AltCtrl => (true, true, false, false), - Mods::AltMeta => (true, false, true, false), - Mods::AltShift => (true, false, false, true), - Mods::CtrlMeta => (false, true, true, false), - Mods::CtrlShift => (false, true, false, true), - Mods::MetaShift => (false, false, true, true), - Mods::AltCtrlMeta => (true, true, true, false), - Mods::AltMetaShift => (true, false, true, true), - Mods::AltCtrlShift => (true, true, false, true), - Mods::CtrlMetaShift => (false, true, true, true), - Mods::AltCtrlMetaShift => (true, true, true, true), - }; - let mut mods = Modifiers::empty(); - mods.set(Modifiers::ALT, alt); - mods.set(Modifiers::CONTROL, ctrl); - mods.set(Modifiers::SUPER, meta); - mods.set(Modifiers::SHIFT, shift); - mods - } -} - -#[derive(Debug, Clone)] -pub struct AcceleratorParseError(String); - -impl std::fmt::Display for AcceleratorParseError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[AcceleratorParseError]: {}", self.0) - } -} - -fn parse_accelerator(accelerator_string: &str) -> Result { +fn parse_accelerator(accelerator_string: &str) -> crate::Result { let mut mods = Modifiers::empty(); let mut key = Code::Unidentified; for raw in accelerator_string.split('+') { let token = raw.trim().to_string(); if token.is_empty() { - return Err(AcceleratorParseError( + return Err(crate::Error::AcceleratorParseError( "Unexpected empty token while parsing accelerator".into(), )); } @@ -165,7 +87,7 @@ fn parse_accelerator(accelerator_string: &str) -> Result only one main key should be allowd. // 2. "Ctrl+C+Shift" => wrong order - return Err(AcceleratorParseError(format!( + return Err(crate::Error::AcceleratorParseError(format!( "Unexpected accelerator string format: \"{}\"", accelerator_string ))); @@ -179,14 +101,14 @@ fn parse_accelerator(accelerator_string: &str) -> Result { - mods.set(Modifiers::SUPER, true); + mods.set(Modifiers::META, true); } "SHIFT" => { mods.set(Modifiers::SHIFT, true); } "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => { #[cfg(target_os = "macos")] - mods.set(Modifiers::SUPER, true); + mods.set(Modifiers::META, true); #[cfg(not(target_os = "macos"))] mods.set(Modifiers::CONTROL, true); } @@ -194,7 +116,7 @@ fn parse_accelerator(accelerator_string: &str) -> Result { - return Err(AcceleratorParseError(format!( + return Err(crate::Error::AcceleratorParseError(format!( "Couldn't identify \"{}\" as a valid `Code`", token ))) @@ -202,7 +124,7 @@ fn parse_accelerator(accelerator_string: &str) -> Result key = code, } } else { - return Err(AcceleratorParseError(format!( + return Err(crate::Error::AcceleratorParseError(format!( "Couldn't identify \"{}\" as a valid `Code`", token ))); @@ -240,7 +162,7 @@ fn test_parse_accelerator() { assert_eq!( parse_accelerator("super+ctrl+SHIFT+alt+ArrowUp").unwrap(), Accelerator { - mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT, + mods: Modifiers::META | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT, key: Code::ArrowUp, } ); @@ -276,7 +198,7 @@ fn test_parse_accelerator() { parse_accelerator("CmdOrCtrl+Space").unwrap(), Accelerator { #[cfg(target_os = "macos")] - mods: Modifiers::SUPER, + mods: Modifiers::META, #[cfg(not(target_os = "macos"))] mods: Modifiers::CONTROL, key: Code::Space, diff --git a/src/check_menu_item.rs b/src/check_menu_item.rs new file mode 100644 index 0000000..2dad7d5 --- /dev/null +++ b/src/check_menu_item.rs @@ -0,0 +1,80 @@ +use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType}; + +/// A check menu item inside a [`Menu`] or [`Submenu`] +/// and usually contains a text and a check mark or a similar toggle +/// that corresponds to a checked and unchecked states. +/// +/// [`Menu`]: crate::Menu +/// [`Submenu`]: crate::Submenu +#[derive(Clone)] +pub struct CheckMenuItem(pub(crate) crate::platform_impl::CheckMenuItem); + +unsafe impl MenuItemExt for CheckMenuItem { + fn type_(&self) -> MenuItemType { + MenuItemType::Check + } + fn as_any(&self) -> &(dyn std::any::Any + 'static) { + self + } + + fn id(&self) -> u32 { + self.id() + } +} + +impl CheckMenuItem { + /// Create a new check menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&` + pub fn new>( + text: S, + enabled: bool, + checked: bool, + acccelerator: Option, + ) -> Self { + Self(crate::platform_impl::CheckMenuItem::new( + text.as_ref(), + enabled, + checked, + acccelerator, + )) + } + + /// Returns a unique identifier associated with this submenu. + pub fn id(&self) -> u32 { + self.0.id() + } + + /// Get the text for this check menu item. + pub fn text(&self) -> String { + self.0.text() + } + + /// Get the text for this check menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&` + pub fn set_text>(&self, text: S) { + self.0.set_text(text.as_ref()) + } + + /// Get whether this check menu item is enabled or not. + pub fn is_enabled(&self) -> bool { + self.0.is_enabled() + } + + /// Enable or disable this check menu item. + pub fn set_enabled(&self, enabled: bool) { + self.0.set_enabled(enabled) + } + + /// Get whether this check menu item is checked or not. + pub fn is_checked(&self) -> bool { + self.0.is_checked() + } + + /// Check or Uncheck this check menu item. + pub fn set_checked(&self, checked: bool) { + self.0.set_checked(checked) + } +} diff --git a/src/counter.rs b/src/counter.rs deleted file mode 100644 index 8408a9d..0000000 --- a/src/counter.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![allow(unused)] - -use std::sync::atomic::{AtomicU64, Ordering}; - -pub struct Counter(AtomicU64); - -impl Counter { - pub const fn new() -> Self { - Self(AtomicU64::new(1)) - } - - pub const fn new_with_start(start: u64) -> Self { - Self(AtomicU64::new(start)) - } - - pub fn next(&self) -> u64 { - self.0.fetch_add(1, Ordering::Relaxed) - } - - pub fn current(&self) -> u64 { - self.0.load(Ordering::Relaxed) - } -} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..62c4a00 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +/// Errors returned by muda. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum Error { + #[error("This menu item is not a child of this `Menu` or `Submenu`")] + NotAChildOfThisMenu, + #[cfg(windows)] + #[error("This menu has not been initialized for this hwnd`")] + NotInitialized, + #[cfg(target_os = "linux")] + #[error("This menu has not been initialized for this gtk window`")] + NotInitialized, + #[error("{0}")] + AcceleratorParseError(String), + #[error("Cannot map {0} to gdk key")] + AcceleratorKeyNotSupported(keyboard_types::Code), +} + +/// Convenient type alias of Result type for muda. +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 3c2c9d7..0a13730 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,498 +1,230 @@ //! muda is a Menu Utilities library for Desktop Applications. -//! # Creating root menus //! -//! Before you can add submenus and menu items, you first need a root or a base menu. +//! # Example +//! +//! Create the menu and add your items +//! //! ```no_run -//! let mut menu = muda::Menu::new(); +//! # use muda::{Menu, Submenu, MenuItem, accelerator::{Code, Modifiers, Accelerator}, PredefinedMenuItem}; +//! let menu = Menu::new(); +//! let menu_item2 = MenuItem::new("Menu item #2", false, None); +//! let submenu = Submenu::with_items( +//! "Submenu Outer", +//! true, +//! &[ +//! &MenuItem::new( +//! "Menu item #1", +//! true, +//! Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD)), +//! ), +//! &PredefinedMenuItem::separator(), +//! &menu_item2, +//! &MenuItem::new("Menu item #3", true, None), +//! &PredefinedMenuItem::separator(), +//! &Submenu::with_items( +//! "Submenu Inner", +//! true, +//! &[ +//! &MenuItem::new("Submenu item #1", true, None), +//! &PredefinedMenuItem::separator(), +//! &menu_item2, +//! ], +//! ), +//! ], +//! ); //! ``` //! -//! # Adding submens to the root menu +//! Then Add your root menu to a Window on Windows and Linux Only or use it +//! as your global app menu on macOS //! -//! Once you have a root menu you can start adding [`Submenu`]s by using [`Menu::add_submenu`]. //! ```no_run -//! let mut menu = muda::Menu::new(); -//! let file_menu = menu.add_submenu("File", true); -//! let edit_menu = menu.add_submenu("Edit", true); -//! ``` -//! -//! # Aadding menu items and submenus within another submenu -//! -//! Once you have a [`Submenu`] you can star creating more [`Submenu`]s or [`MenuItem`]s. -//! ```no_run -//! let mut menu = muda::Menu::new(); -//! -//! let mut file_menu = menu.add_submenu("File", true); -//! let open_item = file_menu.add_item("Open", true, None); -//! let save_item = file_menu.add_item("Save", true, None); -//! -//! let mut edit_menu = menu.add_submenu("Edit", true); -//! let copy_item = file_menu.add_item("Copy", true, None); -//! let cut_item = file_menu.add_item("Cut", true, None); -//! ``` -//! -//! # Add your root menu to a Window (Windows and Linux Only) -//! -//! You can use [`Menu`] to display a top menu in a Window on Windows and Linux. -//! ```ignore -//! let mut menu = muda::Menu::new(); +//! # let menu = muda::Menu::new(); +//! # let window_hwnd = 0; +//! # #[cfg(target_os = "linux")] +//! # let gtk_window = gtk::ApplicationWindow::builder().build(); //! // --snip-- //! #[cfg(target_os = "windows")] -//! menu.init_for_hwnd(window.hwnd() as isize); +//! menu.init_for_hwnd(window_hwnd); //! #[cfg(target_os = "linux")] //! menu.init_for_gtk_window(>k_window); //! #[cfg(target_os = "macos")] //! menu.init_for_nsapp(); //! ``` //! +//! # Context menus (Popup menus) +//! +//! You can also use a [`Menu`] or a [`Submenu`] show a context menu. +//! +//! ```no_run +//! use muda::ContextMenu; +//! # let menu = muda::Menu::new(); +//! # let window_hwnd = 0; +//! # #[cfg(target_os = "linux")] +//! # let gtk_window = gtk::ApplicationWindow::builder().build(); +//! # #[cfg(target_os = "macos")] +//! # let nsview = 0 as *mut objc::runtime::Object; +//! // --snip-- +//! let x = 100.0; +//! let y = 120.0; +//! #[cfg(target_os = "windows")] +//! menu.show_context_menu_for_hwnd(window_hwnd, x, y); +//! #[cfg(target_os = "linux")] +//! menu.show_context_menu_for_gtk_window(>k_window, x, y); +//! #[cfg(target_os = "macos")] +//! menu.show_context_menu_for_nsview(nsview, x, y); +//! ``` //! # Processing menu events //! //! You can use [`menu_event_receiver`] to get a reference to the [`MenuEventReceiver`] //! which you can use to listen to events when a menu item is activated -//! ```ignore -//! if let Ok(event) = muda::menu_event_receiver().try_recv() { +//! ```no_run +//! # use muda::menu_event_receiver; +//! # +//! # let save_item: muda::MenuItem = unsafe { std::mem::zeroed() }; +//! if let Ok(event) = menu_event_receiver().try_recv() { //! match event.id { -//! _ if event.id == save_item.id() => { +//! id if id == save_item.id() => { //! println!("Save menu item activated"); //! }, //! _ => {} //! } //! } //! ``` +//! +//! # Accelerators on Windows +//! +//! Accelerators don't work unless the win32 message loop calls +//! [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) +//! +//! See [`Menu::init_for_hwnd`] for more details -use accelerator::Accelerator; use crossbeam_channel::{unbounded, Receiver, Sender}; use once_cell::sync::Lazy; pub mod accelerator; -mod counter; +mod check_menu_item; +mod error; +mod menu; +mod menu_item; mod platform_impl; +mod predefined; +mod submenu; +mod util; -static MENU_CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| unbounded()); +#[cfg(target_os = "macos")] +#[macro_use] +extern crate objc; -/// Gets a reference to the event channel's [Receiver] -/// which can be used to listen for menu events. -pub fn menu_event_receiver<'a>() -> &'a Receiver { - &MENU_CHANNEL.1 +pub use self::error::*; +pub use check_menu_item::CheckMenuItem; +pub use menu::Menu; +pub use menu_item::MenuItem; +pub use predefined::{AboutMetadata, PredefinedMenuItem}; +pub use submenu::Submenu; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MenuItemType { + Submenu, + Normal, + Check, + Predefined, +} + +impl Default for MenuItemType { + fn default() -> Self { + Self::Normal + } +} + +/// A trait that defines a generic item in a menu, which may be one of [MenuItemType] +/// +/// # Safety +/// +/// This trait is ONLY meant to be implemented internally. +// TODO(amrbashir): first person to replace this trait with an enum while keeping `Menu.append_items` +// taking mix of types (`MenuItem`, `CheckMenuItem`, `Submenu`...etc) in the same call, gets a cookie. +pub unsafe trait MenuItemExt { + /// Get the type of this menu entry + fn type_(&self) -> MenuItemType; + + /// Casts this menu entry to [`Any`](std::any::Any). + /// + /// You can use this to get the concrete underlying type + /// when calling [`Menu::items`] or [`Submenu::items`] by calling [`downcast_ref`](https://doc.rust-lang.org/std/any/trait.Any.html#method.downcast_ref-1) + /// + /// ## Example + /// + /// ```no_run + /// # use muda::{Submenu, MenuItem}; + /// let submenu = Submenu::new("Submenu", true); + /// let item = MenuItem::new("Text", true, None); + /// submenu.append(&item); + /// // --snip-- + /// let item = &submenu.items()[0]; + /// let item = item.as_any().downcast_ref::().unwrap(); + /// item.set_text("New text") + /// ```` + fn as_any(&self) -> &(dyn std::any::Any + 'static); + + /// Returns the id associated with this menu entry + fn id(&self) -> u32; +} + +pub trait ContextMenu { + /// Get the popup [`HMENU`] for this menu. + /// + /// [`HMENU`]: windows_sys::Win32::UI::WindowsAndMessaging::HMENU + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HMENU; + + /// Shows this menu as a context menu inside a win32 window. + /// + /// `x` and `y` are relative to the window's top-left corner. + #[cfg(target_os = "windows")] + fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64); + + /// Attach the menu subclass handler to the given hwnd + /// so you can recieve events from that window using [menu_event_receiver] + /// + /// This can be used along with [`ContextMenu::hpopupmenu`] when implementing a tray icon menu. + #[cfg(target_os = "windows")] + fn attach_menu_subclass_for_hwnd(&self, hwnd: isize); + + /// Remove the menu subclass handler from the given hwnd + #[cfg(target_os = "windows")] + fn detach_menu_subclass_from_hwnd(&self, hwnd: isize); + + /// Shows this menu as a context menu inside a [`gtk::ApplicationWindow`] + /// + /// `x` and `y` are relative to the window's top-left corner. + #[cfg(target_os = "linux")] + fn show_context_menu_for_gtk_window(&self, w: >k::ApplicationWindow, x: f64, y: f64); + /// Get the underlying gtk menu reserved for context menus. + #[cfg(target_os = "linux")] + fn gtk_context_menu(&self) -> gtk::Menu; + + /// Shows this menu as a context menu for the specified `NSView`. + /// + /// The menu will be shown at the coordinates of the current event + /// (the click which triggered the menu to be shown). + #[cfg(target_os = "macos")] + fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64); } /// Describes a menu event emitted when a menu item is activated +#[derive(Debug)] pub struct MenuEvent { /// Id of the menu item which triggered this event - pub id: u64, + pub id: u32, } -/// This is the root menu type to which you can add -/// more submenus and later be add to the top of a window (on Windows and Linux) -/// or used as the menubar menu (on macOS) or displayed as a popup menu. -/// -/// # Example -/// -/// ```no_run -/// let mut menu = muda::Menu::new(); -/// let file_menu = menu.add_submenu("File", true); -/// let edit_menu = menu.add_submenu("Edit", true); -/// ``` -#[derive(Clone)] -pub struct Menu(platform_impl::Menu); +/// A reciever that could be used to listen to menu events. +pub type MenuEventReceiver = Receiver; -impl Menu { - /// Creates a new root menu. - pub fn new() -> Self { - Self(platform_impl::Menu::new()) - } +static MENU_CHANNEL: Lazy<(Sender, MenuEventReceiver)> = Lazy::new(unbounded); - /// Creates a new [`Submenu`] whithin this menu. - /// - /// ## Platform-specific: - /// - /// - **Windows / Linux:** The menu label can contain `&` to indicate which letter should get a generated accelerator. - /// For example, using `&File` for the File menu would result in the label gets an underline under the `F`, - /// and the `&` character is not displayed on menu label. - /// Then the menu can be activated by press `Alt+F`. - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - Submenu(self.0.add_submenu(label, enabled)) - } - - /// 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 - /// similiar methods should be used on the returned [`gtk::Box`]. - /// - /// ## Safety: - /// - /// This should be called before anything is added to the window. - /// - /// ## Panics: - /// - /// Panics if the gtk event loop hasn't been initialized on the thread. - #[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, - { - self.0.init_for_gtk_window(w) - } - - /// Adds this menu to a win32 window. - /// - /// ## Note about accelerators: - /// - /// For accelerators to work, the event loop needs to call - /// [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) - /// with the [`HACCEL`](windows_sys::Win32::UI::WindowsAndMessaging::HACCEL) returned from [`Menu::haccel`] - /// - /// #### Example: - /// ``` - /// # use windows_sys::Win32::UI::WindowsAndMessaging::{MSG, GetMessageW, TranslateMessage, DispatchMessageW }; - /// let menu = Menu::new(); - /// unsafe { - /// let msg: MSG = std::mem::zeroed(); - /// while GetMessageW(&mut msg, 0, 0, 0) == 1 { - /// let translated = TranslateAcceleratorW(msg.hwnd, menu.haccel(), msg); - /// if !translated { - /// TranslateMessage(&msg); - /// DispatchMessageW(&msg); - /// } - /// } - /// } - /// ``` - #[cfg(target_os = "windows")] - pub fn init_for_hwnd(&self, hwnd: isize) { - self.0.init_for_hwnd(hwnd) - } - - /// Returns The [`HACCEL`](windows_sys::Win32::UI::WindowsAndMessaging::HACCEL) associated with this menu - /// It can be used with [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) - /// in the event loop to enable accelerators - #[cfg(target_os = "windows")] - pub fn haccel(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HACCEL { - self.0.haccel() - } - - /// 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, - { - self.0.remove_for_gtk_window(w) - } - - /// Removes this menu from a win32 window - #[cfg(target_os = "windows")] - pub fn remove_for_hwnd(&self, hwnd: isize) { - self.0.remove_for_hwnd(hwnd) - } - - /// 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, - { - self.0.hide_for_gtk_window(w) - } - - /// Hides this menu from a win32 window - #[cfg(target_os = "windows")] - pub fn hide_for_hwnd(&self, hwnd: isize) { - self.0.hide_for_hwnd(hwnd) - } - - /// 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, - { - self.0.show_for_gtk_window(w) - } - - /// Shows this menu from a win32 window - #[cfg(target_os = "windows")] - pub fn show_for_hwnd(&self, hwnd: isize) { - self.0.show_for_hwnd(hwnd) - } - - /// Adds this menu to an NSApp. - #[cfg(target_os = "macos")] - pub fn init_for_nsapp(&self) { - self.0.init_for_nsapp() - } - - /// Removes this menu from an NSApp. - #[cfg(target_os = "macos")] - pub fn remove_for_nsapp(&self) { - self.0.remove_for_nsapp() - } -} - -/// This is a Submenu within another [`Submenu`] or [`Menu`]. -#[derive(Clone)] -pub struct Submenu(platform_impl::Submenu); - -impl Submenu { - /// Gets the submenus's current label. - pub fn label(&self) -> String { - self.0.label() - } - - /// Sets a new label for the submenu. - pub fn set_label>(&mut self, label: S) { - self.0.set_label(label) - } - - /// Gets the submenu's current state, whether enabled or not. - pub fn enabled(&self) -> bool { - self.0.enabled() - } - - /// Enables or disables the submenu - pub fn set_enabled(&mut self, enabled: bool) { - self.0.set_enabled(enabled) - } - - /// Creates a new [`Submenu`] whithin this submenu. - /// - /// ## Platform-specific: - /// - /// - **Windows / Linux:** The menu label can contain `&` to indicate which letter should get a generated accelerator. - /// For example, using `&File` for the File menu would result in the label gets an underline under the `F`, - /// and the `&` character is not displayed on menu label. - /// Then the menu can be activated by press `F` when its parent menu is active. - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - Submenu(self.0.add_submenu(label, enabled)) - } - - /// Creates a new [`MenuItem`] whithin this submenu. - /// - /// ## Platform-specific: - /// - /// - **Windows / Linux:** The menu item label can contain `&` to indicate which letter should get a generated accelerator. - /// For example, using `&Save` for the save menu item would result in the label gets an underline under the `S`, - /// and the `&` character is not displayed on menu item label. - /// Then the menu item can be activated by press `S` when its parent menu is active. - pub fn add_item>( - &mut self, - label: S, - enabled: bool, - accelerator: Option, - ) -> MenuItem { - MenuItem(self.0.add_item(label, enabled, accelerator)) - } - - /// Creates a new [`NativeMenuItem`] within this submenu. - pub fn add_native_item(&mut self, item: NativeMenuItem) { - self.0.add_native_item(item) - } - - /// Creates a new [`CheckMenuItem`] within this submenu. - pub fn add_check_item>( - &mut self, - label: S, - enabled: bool, - checked: bool, - accelerator: Option, - ) -> CheckMenuItem { - CheckMenuItem(self.0.add_check_item(label, enabled, checked, accelerator)) - } -} - -/// This is a normal menu item within a [`Submenu`]. -#[derive(Clone)] -pub struct MenuItem(platform_impl::MenuItem); - -impl MenuItem { - /// Gets the menu item's current label. - pub fn label(&self) -> String { - self.0.label() - } - - /// Sets a new label for the menu item. - pub fn set_label>(&mut self, label: S) { - self.0.set_label(label) - } - - /// Gets the menu item's current state, whether enabled or not. - pub fn enabled(&self) -> bool { - self.0.enabled() - } - - /// Enables or disables the menu item. - pub fn set_enabled(&mut self, enabled: bool) { - self.0.set_enabled(enabled) - } - - /// Gets the unique id for this menu item. - pub fn id(&self) -> u64 { - self.0.id() - } -} - -/// This is a menu item with a checkmark icon within a [`Submenu`]. -#[derive(Clone)] -pub struct CheckMenuItem(platform_impl::CheckMenuItem); - -impl CheckMenuItem { - /// Gets the menu item's current label. - pub fn label(&self) -> String { - self.0.label() - } - - /// Sets a new label for the menu item. - pub fn set_label>(&mut self, label: S) { - self.0.set_label(label) - } - - /// Gets the menu item's current state, whether enabled or not. - pub fn enabled(&self) -> bool { - self.0.enabled() - } - - /// Enables or disables the menu item. - pub fn set_enabled(&mut self, enabled: bool) { - self.0.set_enabled(enabled) - } - - /// Gets the menu item's current state, whether checked or not. - pub fn checked(&self) -> bool { - self.0.checked() - } - - /// Enables or disables the menu item. - pub fn set_checked(&mut self, checked: bool) { - self.0.set_checked(checked) - } - - /// Gets the unique id for this menu item. - pub fn id(&self) -> u64 { - self.0.id() - } -} - -/// This is a Native menu item within a [`Submenu`] with a predefined behavior. -#[non_exhaustive] -#[derive(PartialEq, Eq, 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. - 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 "Toggle fullscreen" menu item. - /// - /// ## platform-specific: - /// - /// - **Windows / Linux:** Unsupported. - ToggleFullScreen, - /// 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(PartialEq, Eq, 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, +/// Gets a reference to the event channel's [MenuEventReceiver] +/// which can be used to listen for menu events. +pub fn menu_event_receiver<'a>() -> &'a MenuEventReceiver { + &MENU_CHANNEL.1 } diff --git a/src/menu.rs b/src/menu.rs new file mode 100644 index 0000000..c6d2b11 --- /dev/null +++ b/src/menu.rs @@ -0,0 +1,261 @@ +use crate::{ContextMenu, MenuItemExt}; + +/// A root menu that can be added to a Window on Windows and Linux +/// and used as the app global menu on macOS. +#[derive(Clone)] +pub struct Menu(crate::platform_impl::Menu); + +impl Default for Menu { + fn default() -> Self { + Self::new() + } +} + +impl ContextMenu for Menu { + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HMENU { + self.0.hpopupmenu() + } + + #[cfg(target_os = "windows")] + fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { + self.0.show_context_menu_for_hwnd(hwnd, x, y) + } + + #[cfg(target_os = "windows")] + fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) { + self.0.attach_menu_subclass_for_hwnd(hwnd) + } + + #[cfg(target_os = "windows")] + fn detach_menu_subclass_from_hwnd(&self, hwnd: isize) { + self.0.detach_menu_subclass_from_hwnd(hwnd) + } + + #[cfg(target_os = "linux")] + fn show_context_menu_for_gtk_window(&self, w: >k::ApplicationWindow, x: f64, y: f64) { + self.0.show_context_menu_for_gtk_window(w, x, y) + } + + #[cfg(target_os = "linux")] + fn gtk_context_menu(&self) -> gtk::Menu { + self.0.gtk_context_menu() + } + + #[cfg(target_os = "macos")] + fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64) { + self.0.show_context_menu_for_nsview(view, x, y) + } +} + +impl Menu { + /// Creates a new menu. + pub fn new() -> Self { + Self(crate::platform_impl::Menu::new()) + } + + /// Creates a new menu with given `items`. It calls [`Menu::new`] and [`Menu::append_items`] internally. + pub fn with_items(items: &[&dyn MenuItemExt]) -> Self { + let menu = Self::new(); + menu.append_items(items); + menu + } + + /// Add a menu item to the end of this menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: crate::Submenu + pub fn append(&self, item: &dyn MenuItemExt) { + self.0.append(item) + } + + /// Add menu items to the end of this menu. It calls [`Menu::append`] in a loop internally. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: crate::Submenu + pub fn append_items(&self, items: &[&dyn MenuItemExt]) { + for item in items { + self.append(*item); + } + } + + /// Add a menu item to the beginning of this menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: crate::Submenu + pub fn prepend(&self, item: &dyn MenuItemExt) { + self.0.prepend(item) + } + + /// Add menu items to the beginning of this menu. It calls [`Menu::insert_items`] with position of `0` internally. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: crate::Submenu + pub fn prepend_items(&self, items: &[&dyn MenuItemExt]) { + self.insert_items(items, 0); + } + + /// Insert a menu item at the specified `postion` in the menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: crate::Submenu + pub fn insert(&self, item: &dyn MenuItemExt, position: usize) { + self.0.insert(item, position) + } + + /// Insert menu items at the specified `postion` in the menu. + /// + /// ## Platform-spcific: + /// + /// - **macOS:** Only [`Submenu`] can be added to the menu + /// + /// [`Submenu`]: crate::Submenu + pub fn insert_items(&self, items: &[&dyn MenuItemExt], position: usize) { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i) + } + } + + /// Remove a menu item from this menu. + pub fn remove(&self, item: &dyn MenuItemExt) -> crate::Result<()> { + self.0.remove(item) + } + + /// Returns a list of menu items that has been added to this menu. + pub fn items(&self) -> Vec> { + self.0.items() + } + + /// 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 + /// similiar methods should be used on the returned [`gtk::Box`]. + /// + /// ## Safety: + /// + /// This should be called before anything is added to the window. + /// + /// ## Panics: + /// + /// Panics if the gtk event loop hasn't been initialized on the thread. + #[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, + { + self.0.init_for_gtk_window(w) + } + + /// Adds this menu to a win32 window. + /// + /// ## Note about accelerators: + /// + /// For accelerators to work, the event loop needs to call + /// [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) + /// with the [`HACCEL`](windows_sys::Win32::UI::WindowsAndMessaging::HACCEL) returned from [`Menu::haccel`] + /// + /// #### Example: + /// ```no_run + /// # use muda::Menu; + /// # use windows_sys::Win32::UI::WindowsAndMessaging::{MSG, GetMessageW, TranslateMessage, DispatchMessageW, TranslateAcceleratorW}; + /// let menu = Menu::new(); + /// unsafe { + /// let mut msg: MSG = std::mem::zeroed(); + /// while GetMessageW(&mut msg, 0, 0, 0) == 1 { + /// let translated = TranslateAcceleratorW(msg.hwnd, menu.haccel(), &msg as *const _); + /// if translated != 1{ + /// TranslateMessage(&msg); + /// DispatchMessageW(&msg); + /// } + /// } + /// } + /// ``` + #[cfg(target_os = "windows")] + pub fn init_for_hwnd(&self, hwnd: isize) { + self.0.init_for_hwnd(hwnd) + } + + /// Returns The [`HACCEL`](windows_sys::Win32::UI::WindowsAndMessaging::HACCEL) associated with this menu + /// It can be used with [`TranslateAcceleratorW`](windows_sys::Win32::UI::WindowsAndMessaging::TranslateAcceleratorW) + /// in the event loop to enable accelerators + #[cfg(target_os = "windows")] + pub fn haccel(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HACCEL { + self.0.haccel() + } + + /// Removes this menu from a [`gtk::ApplicationWindow`] + #[cfg(target_os = "linux")] + pub fn remove_for_gtk_window(&self, w: &W) -> crate::Result<()> + where + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, + { + self.0.remove_for_gtk_window(w) + } + + /// Removes this menu from a win32 window + #[cfg(target_os = "windows")] + pub fn remove_for_hwnd(&self, hwnd: isize) -> crate::Result<()> { + self.0.remove_for_hwnd(hwnd) + } + + /// Hides this menu from a [`gtk::ApplicationWindow`] + #[cfg(target_os = "linux")] + pub fn hide_for_gtk_window(&self, w: &W) -> crate::Result<()> + where + W: gtk::prelude::IsA, + { + self.0.hide_for_gtk_window(w) + } + + /// Hides this menu from a win32 window + #[cfg(target_os = "windows")] + pub fn hide_for_hwnd(&self, hwnd: isize) -> crate::Result<()> { + self.0.hide_for_hwnd(hwnd) + } + + /// Shows this menu on a [`gtk::ApplicationWindow`] + #[cfg(target_os = "linux")] + pub fn show_for_gtk_window(&self, w: &W) -> crate::Result<()> + where + W: gtk::prelude::IsA, + { + self.0.show_for_gtk_window(w) + } + + /// Shows this menu on a win32 window + #[cfg(target_os = "windows")] + pub fn show_for_hwnd(&self, hwnd: isize) -> crate::Result<()> { + self.0.show_for_hwnd(hwnd) + } + + /// Adds this menu to an NSApp. + #[cfg(target_os = "macos")] + pub fn init_for_nsapp(&self) { + self.0.init_for_nsapp() + } + + /// Removes this menu from an NSApp. + #[cfg(target_os = "macos")] + pub fn remove_for_nsapp(&self) { + self.0.remove_for_nsapp() + } +} diff --git a/src/menu_item.rs b/src/menu_item.rs new file mode 100644 index 0000000..72b116d --- /dev/null +++ b/src/menu_item.rs @@ -0,0 +1,62 @@ +use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType}; + +/// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. +/// +/// [`Menu`]: crate::Menu +/// [`Submenu`]: crate::Submenu +#[derive(Clone)] +pub struct MenuItem(pub(crate) crate::platform_impl::MenuItem); + +unsafe impl MenuItemExt for MenuItem { + fn type_(&self) -> MenuItemType { + MenuItemType::Normal + } + fn as_any(&self) -> &(dyn std::any::Any + 'static) { + self + } + + fn id(&self) -> u32 { + self.id() + } +} + +impl MenuItem { + /// Create a new menu item. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&` + pub fn new>(text: S, enabled: bool, acccelerator: Option) -> Self { + Self(crate::platform_impl::MenuItem::new( + text.as_ref(), + enabled, + acccelerator, + )) + } + + /// Returns a unique identifier associated with this menu item. + pub fn id(&self) -> u32 { + self.0.id() + } + + /// Get the text for this menu item. + pub fn text(&self) -> String { + self.0.text() + } + + /// Set the text for this menu item. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&` + pub fn set_text>(&self, text: S) { + self.0.set_text(text.as_ref()) + } + + /// Get whether this menu item is enabled or not. + pub fn is_enabled(&self) -> bool { + self.0.is_enabled() + } + + /// Enable or disable this menu item. + pub fn set_enabled(&self, enabled: bool) { + self.0.set_enabled(enabled) + } +} diff --git a/src/platform_impl/linux/accelerator.rs b/src/platform_impl/gtk/accelerator.rs similarity index 85% rename from src/platform_impl/linux/accelerator.rs rename to src/platform_impl/gtk/accelerator.rs index b2e4449..981e59c 100644 --- a/src/platform_impl/linux/accelerator.rs +++ b/src/platform_impl/gtk/accelerator.rs @@ -3,20 +3,25 @@ use keyboard_types::{Code, Modifiers}; use crate::accelerator::Accelerator; -pub fn to_gtk_menemenoic>(string: S) -> String { +pub fn to_gtk_mnemonic>(string: S) -> String { string .as_ref() .replace("&&", "[~~]") - .replace("&", "_") + .replace('&', "_") .replace("[~~]", "&&") + .replace("[~~]", "&") } -pub fn register_accelerator>( - item: &M, - accel_group: &AccelGroup, - menu_key: &Accelerator, -) { - let accel_key = match &menu_key.key { +pub fn from_gtk_mnemonic>(string: S) -> String { + string + .as_ref() + .replace("__", "[~~]") + .replace('_', "&") + .replace("[~~]", "__") +} + +pub fn parse_accelerator(accelerator: &Accelerator) -> crate::Result<(gdk::ModifierType, u32)> { + let key = match &accelerator.key { Code::KeyA => 'A' as u32, Code::KeyB => 'B' as u32, Code::KeyC => 'C' as u32, @@ -69,19 +74,22 @@ pub fn register_accelerator>( if let Some(gdk_key) = key_to_raw_key(k) { *gdk_key } else { - dbg!("Cannot map key {:?}", k); - return; + return Err(crate::Error::AcceleratorKeyNotSupported(*k)); } } }; - item.add_accelerator( - "activate", - accel_group, - accel_key, - modifiers_to_gdk_modifier_type(menu_key.mods), - gtk::AccelFlags::VISIBLE, - ); + Ok((modifiers_to_gdk_modifier_type(accelerator.mods), key)) +} + +pub fn register_accelerator>( + item: &M, + accel_group: &AccelGroup, + accelerator: &Accelerator, +) { + if let Ok((mods, key)) = parse_accelerator(accelerator) { + item.add_accelerator("activate", accel_group, key, mods, gtk::AccelFlags::VISIBLE); + } } fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType { diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs new file mode 100644 index 0000000..415769d --- /dev/null +++ b/src/platform_impl/gtk/mod.rs @@ -0,0 +1,1691 @@ +mod accelerator; + +use crate::{ + accelerator::Accelerator, + predefined::PredfinedMenuItemType, + util::{AddOp, Counter}, +}; +use accelerator::{from_gtk_mnemonic, parse_accelerator, register_accelerator, to_gtk_mnemonic}; +use gtk::{prelude::*, Orientation}; +use std::{ + cell::RefCell, + collections::HashMap, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; + +static COUNTER: Counter = Counter::new(); + +/// Generic shared type describing a menu entry. It can be one of [`MenuItemType`] +#[derive(Debug, Default)] +pub(crate) struct MenuEntry { + text: String, + enabled: bool, + checked: bool, + id: u32, + accelerator: Option, + type_: MenuItemType, + entries: Option>>>, + + context_menu: (u32, Option), +} + +type GtkSubmenusStore = Vec<(gtk::MenuItem, gtk::Menu, Option>, u32)>; + +/// Be careful when cloning this type, use it only to match against the enum +/// and don't mutate the vectors but it is fine to clone it and +/// call the gtk methods on the elements +#[derive(Debug, Clone)] +enum MenuItemType { + // because gtk doesn't allow using the same [`gtk::MenuItem`] + // multiple times, and thus can't be used in multiple windows, each item + // keeps a hashmap where the key is the id of its parent menu or menubar + // and the value is a vector of a [`gtk::MenuItem`] and related data for this item inside + // that parent menu + Submenu(HashMap), + Normal(HashMap>), + Check { + store: HashMap>, + /// A check menu item can be present in multiple menus and menubars + /// so we have to sync their status when one of them is clicked using the mouse + /// by calling [`gtk::CheckMenuItemExt::set_active`] which will cause an infinte loop + /// of dispatching the same event and trying to sync other instances. + /// This flags ensure we don't end up in an infinite loop. + is_syncing: Rc, + }, + Predefined(HashMap>, PredfinedMenuItemType), +} + +impl Default for MenuItemType { + fn default() -> Self { + Self::Normal(Default::default()) + } +} + +struct InnerMenu { + entries: Vec>>, + // 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. + native_menus: HashMap, Rc)>, + accel_group: Option>, + + context_menu: (u32, Option), +} + +#[derive(Clone)] +pub struct Menu(Rc>); + +impl Menu { + pub fn new() -> Self { + Self(Rc::new(RefCell::new(InnerMenu { + entries: Vec::new(), + native_menus: HashMap::new(), + accel_group: None, + context_menu: (COUNTER.next(), None), + }))) + } + + pub fn append(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Append) + } + + pub fn prepend(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Insert(0)) + } + + pub fn insert(&self, item: &dyn crate::MenuItemExt, position: usize) { + self.add_menu_item(item, AddOp::Insert(position)) + } + + fn add_menu_item(&self, item: &dyn crate::MenuItemExt, op: AddOp) { + let entry = match item.type_() { + crate::MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let entry = &submenu.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + add_gtk_submenu( + menu_bar, + &self.0.borrow().accel_group.as_ref(), + *menu_id, + entry, + op, + true, + ); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_submenu(menu, &None, self.0.borrow().context_menu.0, entry, op, true); + } + + entry + } + crate::MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + add_gtk_text_menuitem( + menu_bar, + *menu_id, + entry, + self.0.borrow().accel_group.as_ref().map(|a| a.as_ref()), + op, + true, + ); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_text_menuitem( + menu, + self.0.borrow().context_menu.0, + entry, + None, + op, + true, + ); + } + + entry + } + crate::MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + add_gtk_predefined_menuitm( + menu_bar, + *menu_id, + entry, + self.0.borrow().accel_group.as_ref().map(|a| a.as_ref()), + op, + true, + ); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_predefined_menuitm( + menu, + self.0.borrow().context_menu.0, + entry, + None, + op, + true, + ); + } + + entry + } + crate::MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + add_gtk_check_menuitem( + menu_bar, + *menu_id, + entry, + self.0.borrow().accel_group.as_ref().map(|a| a.as_ref()), + op, + true, + ) + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_check_menuitem( + menu, + self.0.borrow().context_menu.0, + entry, + None, + op, + true, + ); + } + + entry + } + }; + + let mut inner = self.0.borrow_mut(); + match op { + AddOp::Append => inner.entries.push(entry.clone()), + AddOp::Insert(position) => inner.entries.insert(position, entry.clone()), + } + } + + pub fn remove(&self, item: &dyn crate::MenuItemExt) -> crate::Result<()> { + match item.type_() { + crate::MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let entry = &submenu.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + for item in submenu.items() { + submenu.0.remove_gtk_by_parent_id(*menu_id, &*item); + } + + if let MenuItemType::Submenu(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(menu_id) { + for (item, _, _, _) in items { + menu_bar.remove(&item); + } + } + } + } + } + + if let MenuItemType::Submenu(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for (item, _, _, _) in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + if let MenuItemType::Normal(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu_bar.remove(&item); + } + } + } + } + } + + if let MenuItemType::Normal(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for item in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + if let MenuItemType::Predefined(store, _) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu_bar.remove(&item); + } + } + } + } + } + + if let MenuItemType::Predefined(store, _) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for item in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for (menu_id, (menu_bar, _)) in &self.0.borrow().native_menus { + if let Some(menu_bar) = menu_bar { + if let MenuItemType::Check { store, .. } = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu_bar.remove(&item); + } + } + } + } + } + + if let MenuItemType::Check { store, .. } = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for item in items { + menu.remove(&item); + } + } + } + } + } + }; + + let index = self + .0 + .borrow() + .entries + .iter() + .position(|e| e.borrow().id == item.id()) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + self.0.borrow_mut().entries.remove(index); + Ok(()) + } + + fn remove_gtk_by_parent_id(&self, parent_id: u32, item: &dyn crate::MenuItemExt) { + match item.type_() { + crate::MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let entry = &submenu.0 .0; + if let Some((Some(menu_bar), _)) = self.0.borrow().native_menus.get(&parent_id) { + for item in submenu.items() { + submenu.0.remove_gtk_by_parent_id(parent_id, &*item); + } + + if let MenuItemType::Submenu(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&parent_id) { + for (item, _, _, _) in items { + menu_bar.remove(&item); + } + } + } + } + } + crate::MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + if let Some((Some(menu_bar), _)) = self.0.borrow().native_menus.get(&parent_id) { + if let MenuItemType::Normal(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&parent_id) { + for item in items { + menu_bar.remove(&item); + } + } + } + } + } + crate::MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + if let Some((Some(menu_bar), _)) = self.0.borrow().native_menus.get(&parent_id) { + if let MenuItemType::Predefined(store, _) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&parent_id) { + for item in items { + menu_bar.remove(&item); + } + } + } + } + } + crate::MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + if let Some((Some(menu_bar), _)) = self.0.borrow().native_menus.get(&parent_id) { + if let MenuItemType::Check { store, .. } = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&parent_id) { + for item in items { + menu_bar.remove(&item); + } + } + } + } + } + } + } + + pub fn items(&self) -> Vec> { + self.0 + .borrow() + .entries + .iter() + .map(|e| -> Box { + let entry = e.borrow(); + match entry.type_ { + MenuItemType::Submenu(_) => Box::new(crate::Submenu(Submenu(e.clone()))), + MenuItemType::Normal(_) | MenuItemType::Predefined(_, _) => { + Box::new(crate::MenuItem(MenuItem(e.clone()))) + } + MenuItemType::Check { .. } => { + Box::new(crate::CheckMenuItem(CheckMenuItem(e.clone()))) + } + } + }) + .collect() + } + + pub fn init_for_gtk_window(&self, window: &W) -> Rc + where + W: IsA, + W: IsA, + W: IsA, + { + let mut inner = self.0.borrow_mut(); + let id = window.as_ptr() as u32; + + if inner.accel_group.is_none() { + inner.accel_group = Some(Rc::new(gtk::AccelGroup::new())); + } + + // 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); + window.add(&vbox); + vbox.show(); + inner + .native_menus + .insert(id, (Some(menu_bar), Rc::new(vbox))); + } + + if let Some((menu_bar, vbox)) = inner.native_menus.get(&(id)) { + // 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 we only need to create the menubar + if menu_bar.is_none() { + let vbox = Rc::clone(vbox); + inner + .native_menus + .insert(id, (Some(gtk::MenuBar::new()), vbox)); + } + } + + // Construct the entries of the menubar + let (menu_bar, vbox) = inner.native_menus.get(&id).unwrap(); + let menu_bar = menu_bar.as_ref().unwrap(); + add_entries_to_gtkmenu( + menu_bar, + id, + &inner.entries, + &inner.accel_group.as_ref(), + true, + ); + window.add_accel_group(inner.accel_group.as_ref().unwrap().as_ref()); + + // Show the menubar on the window + vbox.pack_start(menu_bar, false, false, 0); + menu_bar.show(); + + Rc::clone(vbox) + } + + pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> + where + W: IsA, + W: IsA, + { + let id = window.as_ptr() as u32; + let menu_bar = { + let inner = self.0.borrow(); + inner + .native_menus + .get(&id) + .cloned() + .ok_or(crate::Error::NotInitialized)? + }; + + if let (Some(menu_bar), vbox) = menu_bar { + for item in self.items() { + self.remove_gtk_by_parent_id(id, &*item); + } + + let mut inner = self.0.borrow_mut(); + // 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.as_ref().unwrap().as_ref()); + // Remove the removed [`gtk::Menubar`] from our cache + let vbox = Rc::clone(&vbox); + inner.native_menus.insert(id, (None, vbox)); + Ok(()) + } else { + Err(crate::Error::NotInitialized) + } + } + + pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> + where + W: IsA, + { + if let Some((Some(menu_bar), _)) = + self.0.borrow().native_menus.get(&(window.as_ptr() as u32)) + { + menu_bar.hide(); + Ok(()) + } else { + Err(crate::Error::NotInitialized) + } + } + + pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> + where + W: IsA, + { + if let Some((Some(menu_bar), _)) = + self.0.borrow().native_menus.get(&(window.as_ptr() as u32)) + { + menu_bar.show_all(); + Ok(()) + } else { + Err(crate::Error::NotInitialized) + } + } + + pub fn show_context_menu_for_gtk_window(&self, window: &impl IsA, x: f64, y: f64) { + if let Some(window) = window.window() { + let gtk_menu = gtk::Menu::new(); + add_entries_to_gtkmenu(>k_menu, 0, &self.0.borrow().entries, &None, false); + gtk_menu.popup_at_rect( + &window, + &gdk::Rectangle::new(x as _, y as _, 0, 0), + gdk::Gravity::NorthWest, + gdk::Gravity::NorthWest, + None, + ); + } + } + + pub fn gtk_context_menu(&self) -> gtk::Menu { + { + let mut self_ = self.0.borrow_mut(); + if self_.context_menu.1.is_none() { + self_.context_menu.1 = Some(gtk::Menu::new()); + add_entries_to_gtkmenu( + self_.context_menu.1.as_ref().unwrap(), + self_.context_menu.0, + &self_.entries, + &None, + true, + ); + } + } + + self.0.borrow().context_menu.1.as_ref().unwrap().clone() + } +} + +#[derive(Clone)] +pub(crate) struct Submenu(Rc>); + +impl Submenu { + pub fn new(text: &str, enabled: bool) -> Self { + let entry = Rc::new(RefCell::new(MenuEntry { + text: text.to_string(), + enabled, + entries: Some(Vec::new()), + type_: MenuItemType::Submenu(HashMap::new()), + context_menu: (COUNTER.next(), None), + ..Default::default() + })); + + Self(entry) + } + + pub fn id(&self) -> u32 { + self.0.borrow().id + } + + pub fn append(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Append) + } + + pub fn prepend(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Insert(0)) + } + + pub fn insert(&self, item: &dyn crate::MenuItemExt, position: usize) { + self.add_menu_item(item, AddOp::Insert(position)) + } + + fn add_menu_item(&self, item: &dyn crate::MenuItemExt, op: AddOp) { + let type_ = self.0.borrow().type_.clone(); + if let MenuItemType::Submenu(store) = &type_ { + let entry = match item.type_() { + crate::MenuItemType::Submenu => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, accel_group, menu_id) in items { + add_gtk_submenu(menu, &accel_group.as_ref(), *menu_id, entry, op, true); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_submenu( + menu, + &None, + self.0.borrow().context_menu.0, + entry, + op, + true, + ); + } + + entry + } + crate::MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, accel_group, menu_id) in items { + add_gtk_text_menuitem( + menu, + *menu_id, + entry, + accel_group.as_ref().map(|a| a.as_ref()), + op, + true, + ); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_text_menuitem( + menu, + self.0.borrow().context_menu.0, + entry, + None, + op, + true, + ); + } + entry + } + crate::MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, accel_group, menu_id) in items { + add_gtk_predefined_menuitm( + menu, + *menu_id, + entry, + accel_group.as_ref().map(|a| a.as_ref()), + op, + true, + ); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_predefined_menuitm( + menu, + self.0.borrow().context_menu.0, + entry, + None, + op, + true, + ); + } + + entry + } + crate::MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, accel_group, menu_id) in items { + add_gtk_check_menuitem( + menu, + *menu_id, + entry, + accel_group.as_ref().map(|a| a.as_ref()), + op, + true, + ); + } + } + + if let Some(menu) = &self.0.borrow().context_menu.1 { + add_gtk_check_menuitem( + menu, + self.0.borrow().context_menu.0, + entry, + None, + op, + true, + ); + } + entry + } + }; + + let mut inner = self.0.borrow_mut(); + let entries = inner.entries.as_mut().unwrap(); + + match op { + AddOp::Append => entries.push(entry.clone()), + AddOp::Insert(position) => entries.insert(position, entry.clone()), + } + } + } + + pub fn remove(&self, item: &dyn crate::MenuItemExt) -> crate::Result<()> { + if let MenuItemType::Submenu(store) = self.0.borrow().type_.clone() { + match item.type_() { + crate::MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let entry = &submenu.0 .0; + for items in store.values() { + for (_, menu, _, menu_id) in items { + for item in submenu.items() { + submenu.0.remove_gtk_by_parent_id(*menu_id, &*item); + } + + if let MenuItemType::Submenu(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(menu_id) { + for (item, _, _, _) in items { + menu.remove(&item); + } + } + } + } + } + + if let MenuItemType::Submenu(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for (item, _, _, _) in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, _, menu_id) in items { + if let MenuItemType::Normal(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu.remove(&item); + } + } + } + } + } + + if let MenuItemType::Normal(store) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for item in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, _, menu_id) in items { + if let MenuItemType::Predefined(store, _) = + &mut entry.borrow_mut().type_ + { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu.remove(&item); + } + } + } + } + } + + if let MenuItemType::Predefined(store, _) = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for item in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + for items in store.values() { + for (_, menu, _, menu_id) in items { + if let MenuItemType::Check { store, .. } = &mut entry.borrow_mut().type_ + { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu.remove(&item); + } + } + } + } + } + + if let MenuItemType::Check { store, .. } = &mut entry.borrow_mut().type_ { + if let Some(items) = store.remove(&self.0.borrow().context_menu.0) { + if let Some(menu) = &self.0.borrow().context_menu.1 { + for item in items { + menu.remove(&item); + } + } + } + } + } + }; + } + + let index = self + .0 + .borrow() + .entries + .as_ref() + .ok_or(crate::Error::NotAChildOfThisMenu)? + .iter() + .position(|e| e.borrow().id == item.id()) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + self.0.borrow_mut().entries.as_mut().unwrap().remove(index); + + Ok(()) + } + + fn remove_gtk_by_parent_id(&self, parent_id: u32, item: &dyn crate::MenuItemExt) { + if let MenuItemType::Submenu(store) = self.0.borrow().type_.clone() { + match item.type_() { + crate::MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let entry = &submenu.0 .0; + if let Some(items) = store.get(&parent_id) { + for (_, menu, _, menu_id) in items { + for item in submenu.items() { + submenu.0.remove_gtk_by_parent_id(*menu_id, &*item); + } + + if let MenuItemType::Submenu(store) = &mut entry.borrow_mut().type_ { + let items = store.remove(menu_id).unwrap(); + for (item, _, _, _) in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let entry = &item.0 .0; + if let Some(items) = store.get(&parent_id) { + for (_, menu, _, menu_id) in items { + if let MenuItemType::Normal(store) = &mut entry.borrow_mut().type_ { + let items = store.remove(menu_id).unwrap(); + for item in items { + menu.remove(&item); + } + } + } + } + } + crate::MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + if let Some(items) = store.get(&parent_id) { + for (_, menu, _, menu_id) in items { + if let MenuItemType::Predefined(store, _) = + &mut entry.borrow_mut().type_ + { + if let Some(items) = store.remove(menu_id) { + for item in items { + menu.remove(&item); + } + } + } + } + } + } + crate::MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let entry = &item.0 .0; + if let Some(items) = store.get(&parent_id) { + for (_, menu, _, menu_id) in items { + if let MenuItemType::Check { store, .. } = &mut entry.borrow_mut().type_ + { + let items = store.remove(menu_id).unwrap(); + for item in items { + menu.remove(&item); + } + } + } + } + } + }; + } + } + + pub fn items(&self) -> Vec> { + self.0 + .borrow() + .entries + .as_ref() + .unwrap() + .iter() + .map(|e| -> Box { + let entry = e.borrow(); + match entry.type_ { + MenuItemType::Submenu(_) => Box::new(crate::Submenu(Submenu(e.clone()))), + MenuItemType::Normal(_) | MenuItemType::Predefined(_, _) => { + Box::new(crate::MenuItem(MenuItem(e.clone()))) + } + MenuItemType::Check { .. } => { + Box::new(crate::CheckMenuItem(CheckMenuItem(e.clone()))) + } + } + }) + .collect() + } + + pub fn text(&self) -> String { + let entry = self.0.borrow(); + if let MenuItemType::Submenu(store) = &entry.type_ { + store + .get(&0) + .map(|items| items.first()) + .map(|i| { + i.map(|i| { + i.0.label() + .map(|l| l.as_str().to_string()) + .map(from_gtk_mnemonic) + .unwrap_or_default() + }) + .unwrap_or_else(|| entry.text.clone()) + }) + .unwrap_or_else(|| entry.text.clone()) + } else { + unreachable!() + } + } + + pub fn set_text(&self, text: &str) { + let mut entry = self.0.borrow_mut(); + entry.text = text.to_string(); + + if let MenuItemType::Submenu(store) = &entry.type_ { + let text = to_gtk_mnemonic(text); + for items in store.values() { + for (i, _, _, _) in items { + i.set_label(&text); + } + } + } else { + unreachable!() + } + } + + pub fn is_enabled(&self) -> bool { + let entry = self.0.borrow(); + if let MenuItemType::Submenu(store) = &entry.type_ { + store + .get(&0) + .map(|items| items.first()) + .map(|i| { + i.map(|i| i.0.is_sensitive()) + .unwrap_or_else(|| entry.enabled) + }) + .unwrap_or_else(|| entry.enabled) + } else { + unreachable!() + } + } + + pub fn set_enabled(&self, enabled: bool) { + let mut entry = self.0.borrow_mut(); + entry.enabled = enabled; + + if let MenuItemType::Submenu(store) = &entry.type_ { + for items in store.values() { + for (i, _, _, _) in items { + i.set_sensitive(enabled); + } + } + } else { + unreachable!() + } + } + + pub fn show_context_menu_for_gtk_window(&self, window: &impl IsA, x: f64, y: f64) { + if let Some(window) = window.window() { + let gtk_menu = gtk::Menu::new(); + add_entries_to_gtkmenu( + >k_menu, + 0, + self.0.borrow().entries.as_ref().unwrap(), + &None, + false, + ); + gtk_menu.popup_at_rect( + &window, + &gdk::Rectangle::new(x as _, y as _, 0, 0), + gdk::Gravity::NorthWest, + gdk::Gravity::NorthWest, + None, + ); + } + } + + pub fn gtk_context_menu(&self) -> gtk::Menu { + { + let mut self_ = self.0.borrow_mut(); + if self_.context_menu.1.is_none() { + self_.context_menu.1 = Some(gtk::Menu::new()); + add_entries_to_gtkmenu( + self_.context_menu.1.as_ref().unwrap(), + self_.context_menu.0, + self_.entries.as_ref().unwrap(), + &None, + true, + ); + } + } + + self.0.borrow().context_menu.1.as_ref().unwrap().clone() + } +} + +#[derive(Clone)] +pub(crate) struct MenuItem(Rc>); + +impl MenuItem { + pub fn new(text: &str, enabled: bool, accelerator: Option) -> Self { + let entry = Rc::new(RefCell::new(MenuEntry { + text: text.to_string(), + enabled, + accelerator, + id: COUNTER.next(), + type_: MenuItemType::Normal(HashMap::new()), + ..Default::default() + })); + + Self(entry) + } + + pub fn id(&self) -> u32 { + self.0.borrow().id + } + + pub fn text(&self) -> String { + let entry = self.0.borrow(); + if let MenuItemType::Normal(store) = &entry.type_ { + store + .get(&0) + .map(|items| items.first()) + .map(|i| { + i.map(|i| { + i.label() + .map(|l| l.as_str().to_string()) + .map(from_gtk_mnemonic) + .unwrap_or_default() + }) + .unwrap_or_else(|| entry.text.clone()) + }) + .unwrap_or_else(|| entry.text.clone()) + } else { + unreachable!() + } + } + + pub fn set_text(&self, text: &str) { + let mut entry = self.0.borrow_mut(); + entry.text = text.to_string(); + + if let MenuItemType::Normal(store) = &entry.type_ { + let text = to_gtk_mnemonic(text); + for items in store.values() { + for i in items { + i.set_label(&text); + } + } + } else { + unreachable!() + } + } + + pub fn is_enabled(&self) -> bool { + let entry = self.0.borrow(); + if let MenuItemType::Normal(store) = &entry.type_ { + store + .get(&0) + .map(|items| items.first()) + .map(|i| i.map(|i| i.is_sensitive()).unwrap_or_else(|| entry.enabled)) + .unwrap_or_else(|| entry.enabled) + } else { + unreachable!() + } + } + + pub fn set_enabled(&self, enabled: bool) { + let mut entry = self.0.borrow_mut(); + entry.enabled = enabled; + + if let MenuItemType::Normal(store) = &entry.type_ { + for items in store.values() { + for i in items { + i.set_sensitive(enabled); + } + } + } else { + unreachable!() + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PredefinedMenuItem(Rc>); + +impl PredefinedMenuItem { + pub fn new(item: PredfinedMenuItemType, text: Option) -> Self { + let entry = Rc::new(RefCell::new(MenuEntry { + text: text.unwrap_or_else(|| item.text().to_string()), + enabled: true, + accelerator: item.accelerator(), + id: COUNTER.next(), + type_: MenuItemType::Predefined(HashMap::new(), item), + ..Default::default() + })); + + Self(entry) + } + + pub fn id(&self) -> u32 { + self.0.borrow().id + } + + pub fn text(&self) -> String { + let entry = self.0.borrow(); + if let MenuItemType::Predefined(store, _) = &entry.type_ { + store + .get(&0) + .map(|items| items.get(0)) + .map(|i| { + i.map(|i| { + i.label() + .map(|l| l.as_str().to_string()) + .map(from_gtk_mnemonic) + .unwrap_or_default() + }) + .unwrap_or_else(|| entry.text.clone()) + }) + .unwrap_or_else(|| entry.text.clone()) + } else { + unreachable!() + } + } + + pub fn set_text(&self, text: &str) { + let mut entry = self.0.borrow_mut(); + entry.text = text.to_string(); + + if let MenuItemType::Normal(store) = &entry.type_ { + let text = to_gtk_mnemonic(text); + for items in store.values() { + for i in items { + i.set_label(&text); + } + } + } else { + unreachable!() + } + } +} + +#[derive(Clone)] +pub(crate) struct CheckMenuItem(Rc>); + +impl CheckMenuItem { + pub fn new(text: &str, enabled: bool, checked: bool, accelerator: Option) -> Self { + let entry = Rc::new(RefCell::new(MenuEntry { + text: text.to_string(), + enabled, + checked, + accelerator, + id: COUNTER.next(), + type_: MenuItemType::Check { + store: HashMap::new(), + is_syncing: Rc::new(AtomicBool::new(false)), + }, + ..Default::default() + })); + + Self(entry) + } + + pub fn id(&self) -> u32 { + self.0.borrow().id + } + + pub fn text(&self) -> String { + let entry = self.0.borrow(); + if let MenuItemType::Check { store, .. } = &entry.type_ { + store + .get(&0) + .map(|items| items.get(0)) + .map(|i| { + i.map(|i| { + i.label() + .map(|l| l.as_str().to_string()) + .map(from_gtk_mnemonic) + .unwrap_or_default() + }) + .unwrap_or_else(|| entry.text.clone()) + }) + .unwrap_or_else(|| entry.text.clone()) + } else { + unreachable!() + } + } + + pub fn set_text(&self, text: &str) { + let mut entry = self.0.borrow_mut(); + entry.text = text.to_string(); + + if let MenuItemType::Check { store, .. } = &entry.type_ { + let text = to_gtk_mnemonic(text); + for items in store.values() { + for i in items { + i.set_label(&text); + } + } + } else { + unreachable!() + } + } + + pub fn is_enabled(&self) -> bool { + let entry = self.0.borrow(); + if let MenuItemType::Check { store, .. } = &entry.type_ { + store + .get(&0) + .map(|items| items.get(0)) + .map(|i| i.map(|i| i.is_sensitive()).unwrap_or(entry.enabled)) + .unwrap_or(entry.enabled) + } else { + unreachable!() + } + } + + pub fn set_enabled(&self, enabled: bool) { + let mut entry = self.0.borrow_mut(); + entry.enabled = enabled; + + if let MenuItemType::Check { store, .. } = &entry.type_ { + for items in store.values() { + for i in items { + i.set_sensitive(enabled); + } + } + } else { + unreachable!() + } + } + + pub fn is_checked(&self) -> bool { + let entry = self.0.borrow(); + if let MenuItemType::Check { store, .. } = &entry.type_ { + store + .get(&0) + .map(|items| items.get(0)) + .map(|i| i.map(|i| i.is_active()).unwrap_or(entry.checked)) + .unwrap_or(entry.checked) + } else { + unreachable!() + } + } + + pub fn set_checked(&self, checked: bool) { + let type_ = { + let mut entry = self.0.borrow_mut(); + entry.checked = checked; + entry.type_.clone() + }; + + if let MenuItemType::Check { store, is_syncing } = &type_ { + is_syncing.store(true, Ordering::Release); + for items in store.values() { + for i in items { + i.set_active(checked); + } + } + is_syncing.store(false, Ordering::Release); + } else { + unreachable!() + } + } +} + +fn add_gtk_submenu( + menu: &impl IsA, + accel_group: &Option<&Rc>, + menu_id: u32, + entry: &Rc>, + op: AddOp, + add_to_store: bool, +) { + let mut entry = entry.borrow_mut(); + let submenu = gtk::Menu::new(); + let item = gtk::MenuItem::builder() + .label(&to_gtk_mnemonic(&entry.text)) + .use_underline(true) + .submenu(&submenu) + .sensitive(entry.enabled) + .build(); + + match op { + AddOp::Append => menu.append(&item), + AddOp::Insert(position) => menu.insert(&item, position as i32), + } + + item.show(); + let id = COUNTER.next(); + add_entries_to_gtkmenu( + &submenu, + id, + entry.entries.as_ref().unwrap(), + accel_group, + add_to_store, + ); + if let MenuItemType::Submenu(store) = &mut entry.type_ { + let item = (item, submenu, accel_group.cloned(), id); + if let Some(items) = store.get_mut(&menu_id) { + items.push(item); + } else { + store.insert(menu_id, vec![item]); + } + } +} + +fn add_gtk_text_menuitem( + menu: &impl IsA, + menu_id: u32, + entry: &Rc>, + accel_group: Option<>k::AccelGroup>, + op: AddOp, + add_to_store: bool, +) { + let mut entry = entry.borrow_mut(); + if let MenuItemType::Normal(_) = &entry.type_ { + let item = gtk::MenuItem::builder() + .label(&to_gtk_mnemonic(&entry.text)) + .use_underline(true) + .sensitive(entry.enabled) + .build(); + let id = entry.id; + + match op { + AddOp::Append => menu.append(&item), + AddOp::Insert(position) => menu.insert(&item, position as i32), + } + + item.show(); + if let Some(accelerator) = &entry.accelerator { + if let Some(accel_group) = accel_group { + register_accelerator(&item, accel_group, accelerator); + } + } + item.connect_activate(move |_| { + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); + }); + + if add_to_store { + if let MenuItemType::Normal(store) = &mut entry.type_ { + if let Some(items) = store.get_mut(&menu_id) { + items.push(item); + } else { + store.insert(menu_id, vec![item]); + } + } + } + } +} + +fn add_gtk_predefined_menuitm( + menu: &impl IsA, + menu_id: u32, + entry: &Rc>, + accel_group: Option<>k::AccelGroup>, + op: AddOp, + add_to_store: bool, +) { + let mut entry = entry.borrow_mut(); + let text = entry.text.clone(); + let accelerator = entry.accelerator.clone(); + + if let MenuItemType::Predefined(store, predefined_item) = &mut entry.type_ { + let predefined_item = predefined_item.clone(); + let make_item = || { + gtk::MenuItem::builder() + .label(&to_gtk_mnemonic(text)) + .use_underline(true) + .sensitive(true) + .build() + }; + let register_accel = |item: >k::MenuItem| { + if let Some(accelerator) = accelerator { + if let Some(accel_group) = accel_group { + register_accelerator(item, accel_group, &accelerator); + } + } + }; + + let item = match predefined_item { + PredfinedMenuItemType::Separator => { + Some(gtk::SeparatorMenuItem::new().upcast::()) + } + PredfinedMenuItemType::Copy + | PredfinedMenuItemType::Cut + | PredfinedMenuItemType::Paste + | PredfinedMenuItemType::SelectAll => { + let item = make_item(); + let (mods, key) = + parse_accelerator(&predefined_item.accelerator().unwrap()).unwrap(); + item.child() + .unwrap() + .downcast::() + .unwrap() + .set_accel(key, mods); + item.connect_activate(move |_| { + // TODO: wayland + if let Ok(xdo) = libxdo::XDo::new(None) { + let _ = xdo.send_keysequence(predefined_item.xdo_keys(), 0); + } + }); + Some(item) + } + PredfinedMenuItemType::About(metadata) => { + let item = make_item(); + register_accel(&item); + item.connect_activate(move |_| { + if let Some(metadata) = &metadata { + let mut builder = gtk::builders::AboutDialogBuilder::new() + .modal(true) + .resizable(false); + + if let Some(name) = &metadata.name { + builder = builder.program_name(name); + } + if let Some(version) = &metadata.version { + builder = builder.version(version); + } + if let Some(authors) = &metadata.authors { + builder = builder.authors(authors.clone()); + } + if let Some(comments) = &metadata.comments { + builder = builder.comments(comments); + } + if let Some(copyright) = &metadata.copyright { + builder = builder.copyright(copyright); + } + if let Some(license) = &metadata.license { + builder = builder.license(license); + } + if let Some(website) = &metadata.website { + builder = builder.website(website); + } + if let Some(website_label) = &metadata.website_label { + builder = builder.website_label(website_label); + } + + let about = builder.build(); + about.run(); + unsafe { + about.destroy(); + } + } + }); + Some(item) + } + _ => None, + }; + + if let Some(item) = item { + match op { + AddOp::Append => menu.append(&item), + AddOp::Insert(position) => menu.insert(&item, position as i32), + } + item.show(); + + if add_to_store { + if let Some(items) = store.get_mut(&menu_id) { + items.push(item); + } else { + store.insert(menu_id, vec![item]); + } + } + } + } +} + +fn add_gtk_check_menuitem( + menu: &impl IsA, + menu_id: u32, + entry: &Rc>, + accel_group: Option<>k::AccelGroup>, + op: AddOp, + add_to_store: bool, +) { + let entry_c = entry.clone(); + let mut entry = entry.borrow_mut(); + + let item = gtk::CheckMenuItem::builder() + .label(&to_gtk_mnemonic(&entry.text)) + .use_underline(true) + .sensitive(entry.enabled) + .active(entry.checked) + .build(); + if let Some(accelerator) = &entry.accelerator { + if let Some(accel_group) = accel_group { + register_accelerator(&item, accel_group, accelerator); + } + } + let id = entry.id; + + item.connect_toggled(move |i| { + let should_dispatch = matches!(&entry_c.borrow().type_, MenuItemType::Check { is_syncing, .. } if is_syncing + .compare_exchange(false, true, Ordering::Release, Ordering::Relaxed) + .is_ok()); + + if should_dispatch { + let checked = i.is_active(); + let type_ = { + let mut entry = entry_c.borrow_mut(); + entry.checked = checked; + entry.type_.clone() + }; + + if let MenuItemType::Check { store, .. } = &type_ { + for items in store.values() { + for i in items { + i.set_active(checked); + } + } + if let MenuItemType::Check { is_syncing, .. } = &mut entry_c.borrow_mut().type_ { + is_syncing.store(false, Ordering::Release); + } + } + + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); + } + }); + + match op { + AddOp::Append => menu.append(&item), + AddOp::Insert(position) => menu.insert(&item, position as i32), + } + + item.show(); + + if add_to_store { + if let MenuItemType::Check { store, .. } = &mut entry.type_ { + if let Some(items) = store.get_mut(&menu_id) { + items.push(item); + } else { + store.insert(menu_id, vec![item]); + } + } + } +} + +fn add_entries_to_gtkmenu>( + menu: &M, + menu_id: u32, + entries: &Vec>>, + accel_group: &Option<&Rc>, + add_to_store: bool, +) { + for entry in entries { + let type_ = entry.borrow().type_.clone(); + match type_ { + MenuItemType::Submenu(_) => add_gtk_submenu( + menu, + accel_group, + menu_id, + entry, + AddOp::Append, + add_to_store, + ), + MenuItemType::Normal(_) => add_gtk_text_menuitem( + menu, + menu_id, + entry, + accel_group.map(|a| a.as_ref()), + AddOp::Append, + add_to_store, + ), + MenuItemType::Predefined(_, _) => add_gtk_predefined_menuitm( + menu, + menu_id, + entry, + accel_group.map(|a| a.as_ref()), + AddOp::Append, + add_to_store, + ), + MenuItemType::Check { .. } => add_gtk_check_menuitem( + menu, + menu_id, + entry, + accel_group.map(|a| a.as_ref()), + AddOp::Append, + add_to_store, + ), + } + } +} + +impl PredfinedMenuItemType { + fn xdo_keys(&self) -> &str { + match self { + PredfinedMenuItemType::Copy => "ctrl+c", + PredfinedMenuItemType::Cut => "ctrl+X", + PredfinedMenuItemType::Paste => "ctrl+v", + PredfinedMenuItemType::SelectAll => "ctrl+a", + _ => "", + } + } +} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs deleted file mode 100644 index 309a1ee..0000000 --- a/src/platform_impl/linux/mod.rs +++ /dev/null @@ -1,690 +0,0 @@ -mod accelerator; - -use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem}; -use accelerator::{register_accelerator, to_gtk_menemenoic}; -use gtk::{prelude::*, Orientation}; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; - -static COUNTER: Counter = Counter::new(); - -/// Generic shared type describing a menu entry. It can be one of [`MenuEntryType`] -#[derive(Debug, Default)] -struct MenuEntry { - label: String, - enabled: bool, - checked: bool, - id: u64, - accelerator: Option, - r#type: MenuEntryType, - entries: Option>>>, -} - -#[derive(PartialEq, Eq, Debug)] -enum MenuEntryType { - // 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`] if its a menu - // and push to it every time [`Menu::init_for_gtk_window`] is called. - Submenu(Vec<(gtk::MenuItem, gtk::Menu)>), - MenuItem(Vec), - CheckMenuItem(Vec), - NativeMenuItem(NativeMenuItem), -} - -impl Default for MenuEntryType { - fn default() -> Self { - Self::MenuItem(Default::default()) - } -} - -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. 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. - native_menus: HashMap, Rc)>, - accel_group: Rc, -} - -#[derive(Clone)] -pub struct Menu(Rc>); - -impl Menu { - pub fn new() -> Self { - Self(Rc::new(RefCell::new(InnerMenu { - entries: Vec::new(), - native_menus: HashMap::new(), - accel_group: Rc::new(gtk::AccelGroup::new()), - }))) - } - - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - let label = label.as_ref().to_string(); - - let entry = Rc::new(RefCell::new(MenuEntry { - label: label.clone(), - enabled, - entries: Some(Vec::new()), - r#type: MenuEntryType::Submenu(Vec::new()), - ..Default::default() - })); - - let mut inner = self.0.borrow_mut(); - for (_, (menu_bar, _)) in inner.native_menus.iter() { - if let Some(menu_bar) = menu_bar { - let (item, submenu) = create_gtk_submenu(&label, enabled); - menu_bar.append(&item); - let mut native_menus = entry.borrow_mut(); - if let MenuEntryType::Submenu(m) = &mut native_menus.r#type { - m.push((item, submenu)); - } - } - } - - inner.entries.push(entry.clone()); - Submenu(entry, Rc::clone(&inner.accel_group)) - } - - 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 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); - window.add(&vbox); - vbox.show(); - inner - .native_menus - .insert(window.as_ptr() as _, (Some(menu_bar), Rc::new(vbox))); - } - - 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 we only need to create the menubar - if menu_bar.is_none() { - let vbox = Rc::clone(vbox); - inner - .native_menus - .insert(window.as_ptr() as _, (Some(gtk::MenuBar::new()), vbox)); - } - } - - // 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, - ); - window.add_accel_group(&*inner.accel_group); - - // Show the menubar on the window - vbox.pack_start(menu_bar.as_ref().unwrap(), false, false, 0); - menu_bar.as_ref().unwrap().show(); - - Rc::clone(vbox) - } - - pub fn remove_for_gtk_window(&self, window: &W) - where - W: IsA, - W: IsA, - { - let mut inner = self.0.borrow_mut(); - - 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 - .native_menus - .insert(window.as_ptr() as _, (None, vbox)); - } - } - - pub fn hide_for_gtk_window(&self, window: &W) - where - W: IsA, - { - 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, window: &W) - where - W: IsA, - { - if let Some((Some(menu_bar), _)) = self - .0 - .borrow() - .native_menus - .get(&(window.as_ptr() as isize)) - { - menu_bar.show_all(); - } - } -} - -#[derive(Clone)] -pub struct Submenu(Rc>, Rc); - -impl Submenu { - pub fn label(&self) -> String { - self.0.borrow().label.clone() - } - - pub fn set_label>(&mut self, label: S) { - let label = label.as_ref().to_string(); - let mut entry = self.0.borrow_mut(); - if let MenuEntryType::Submenu(native_menus) = &mut entry.r#type { - for (item, _) in native_menus { - item.set_label(&to_gtk_menemenoic(&label)); - } - } - entry.label = label; - } - - pub fn enabled(&self) -> bool { - self.0.borrow().enabled - } - - pub fn set_enabled(&mut self, enabled: bool) { - let mut entry = self.0.borrow_mut(); - entry.enabled = true; - if let MenuEntryType::Submenu(native_menus) = &mut entry.r#type { - for (item, _) in native_menus { - item.set_sensitive(enabled); - } - } - } - - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - let label = label.as_ref().to_string(); - - let entry = Rc::new(RefCell::new(MenuEntry { - label: label.clone(), - enabled, - entries: Some(Vec::new()), - r#type: MenuEntryType::Submenu(Vec::new()), - ..Default::default() - })); - - let mut inner = self.0.borrow_mut(); - if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type { - for (_, menu) in native_menus { - let (item, submenu) = create_gtk_submenu(&label, enabled); - menu.append(&item); - if let MenuEntryType::Submenu(menus) = &mut entry.borrow_mut().r#type { - menus.push((item, submenu)); - } - } - } - - inner.entries.as_mut().unwrap().push(entry.clone()); - Submenu(entry, Rc::clone(&self.1)) - } - - pub fn add_item>( - &mut self, - label: S, - enabled: bool, - accelerator: Option, - ) -> MenuItem { - let label = label.as_ref().to_string(); - let id = COUNTER.next(); - - let entry = Rc::new(RefCell::new(MenuEntry { - label: label.clone(), - enabled, - r#type: MenuEntryType::MenuItem(Vec::new()), - id, - accelerator: accelerator.clone(), - ..Default::default() - })); - - let mut inner = self.0.borrow_mut(); - - if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type { - for (_, menu) in native_menus { - let item = create_gtk_menu_item(&label, enabled, &accelerator, id, &*self.1); - menu.append(&item); - if let MenuEntryType::MenuItem(native_items) = &mut entry.borrow_mut().r#type { - native_items.push(item); - } - } - } - - inner.entries.as_mut().unwrap().push(entry.clone()); - MenuItem(entry) - } - - pub fn add_native_item(&mut self, item: NativeMenuItem) { - let mut inner = self.0.borrow_mut(); - - if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type { - for (_, menu) in native_menus { - item.add_to_gtk_menu(menu); - } - } - - let entry = Rc::new(RefCell::new(MenuEntry { - r#type: MenuEntryType::NativeMenuItem(item), - ..Default::default() - })); - inner.entries.as_mut().unwrap().push(entry); - } - - pub fn add_check_item>( - &mut self, - label: S, - enabled: bool, - checked: bool, - accelerator: Option, - ) -> CheckMenuItem { - let label = label.as_ref().to_string(); - let id = COUNTER.next(); - - let entry = Rc::new(RefCell::new(MenuEntry { - label: label.clone(), - enabled, - checked, - r#type: MenuEntryType::CheckMenuItem(Vec::new()), - id, - accelerator: accelerator.clone(), - ..Default::default() - })); - - let mut inner = self.0.borrow_mut(); - - if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type { - for (_, menu) in native_menus { - let item = create_gtk_check_menu_item( - &label, - enabled, - checked, - &accelerator, - id, - &*self.1, - ); - menu.append(&item); - if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.borrow_mut().r#type { - native_items.push(item); - } - } - } - - inner.entries.as_mut().unwrap().push(entry.clone()); - CheckMenuItem(entry) - } -} - -#[derive(Clone)] -pub struct MenuItem(Rc>); - -impl MenuItem { - pub fn label(&self) -> String { - self.0.borrow().label.clone() - } - - pub fn set_label>(&mut self, label: S) { - let label = label.as_ref().to_string(); - let mut entry = self.0.borrow_mut(); - if let MenuEntryType::MenuItem(native_items) = &mut entry.r#type { - for item in native_items { - item.set_label(&to_gtk_menemenoic(&label)); - } - } - entry.label = label; - } - - pub fn enabled(&self) -> bool { - self.0.borrow().enabled - } - - pub fn set_enabled(&mut self, enabled: bool) { - let mut entry = self.0.borrow_mut(); - if let MenuEntryType::MenuItem(native_items) = &mut entry.r#type { - for item in native_items { - item.set_sensitive(enabled); - } - } - entry.enabled = enabled; - } - - pub fn id(&self) -> u64 { - self.0.borrow().id - } -} - -#[derive(Clone)] -pub struct CheckMenuItem(Rc>); - -impl CheckMenuItem { - pub fn label(&self) -> String { - self.0.borrow().label.clone() - } - - pub fn set_label>(&mut self, label: S) { - let label = label.as_ref().to_string(); - let mut entry = self.0.borrow_mut(); - if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.r#type { - for item in native_items { - item.set_label(&to_gtk_menemenoic(&label)); - } - } - entry.label = label; - } - - pub fn enabled(&self) -> bool { - self.0.borrow().enabled - } - - pub fn set_enabled(&mut self, enabled: bool) { - let mut entry = self.0.borrow_mut(); - if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.r#type { - for item in native_items { - item.set_sensitive(enabled); - } - } - entry.enabled = enabled; - } - - pub fn checked(&self) -> bool { - let entry = self.0.borrow(); - let mut checked = entry.checked; - if let MenuEntryType::CheckMenuItem(native_items) = &entry.r#type { - if let Some(item) = native_items.get(0) { - checked = item.is_active(); - } - } - - checked - } - - pub fn set_checked(&mut self, checked: bool) { - let mut entry = self.0.borrow_mut(); - if let MenuEntryType::CheckMenuItem(native_items) = &mut entry.r#type { - for item in native_items { - item.set_active(checked); - } - } - entry.checked = checked; - } - - pub fn id(&self) -> u64 { - self.0.borrow().id - } -} - -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 (item, submenu) = match &mut entry.r#type { - MenuEntryType::Submenu(_) => { - let (item, submenu) = create_gtk_submenu(&entry.label, entry.enabled); - gtk_menu.append(&item); - add_entries_to_menu(&submenu, entry.entries.as_ref().unwrap(), accel_group); - (Some(item), Some(submenu)) - } - MenuEntryType::MenuItem(_) => { - let item = create_gtk_menu_item( - &entry.label, - entry.enabled, - &entry.accelerator, - entry.id, - accel_group, - ); - gtk_menu.append(&item); - (Some(item), None) - } - MenuEntryType::CheckMenuItem(_) => { - let item = create_gtk_check_menu_item( - &entry.label, - entry.enabled, - entry.checked, - &entry.accelerator, - entry.id, - accel_group, - ); - gtk_menu.append(&item); - (Some(item.upcast::()), None) - } - MenuEntryType::NativeMenuItem(native_menu_item) => { - native_menu_item.add_to_gtk_menu(gtk_menu); - (None, None) - } - }; - - match &mut entry.r#type { - MenuEntryType::Submenu(native_menus) => { - native_menus.push((item.unwrap(), submenu.unwrap())); - } - MenuEntryType::MenuItem(native_items) => { - native_items.push(item.unwrap()); - } - MenuEntryType::CheckMenuItem(native_items) => { - native_items.push(item.unwrap().downcast().unwrap()); - } - MenuEntryType::NativeMenuItem(_) => {} - }; - } -} - -fn create_gtk_submenu(label: &str, enabled: bool) -> (gtk::MenuItem, gtk::Menu) { - let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label)); - item.set_sensitive(enabled); - let menu = gtk::Menu::new(); - item.set_submenu(Some(&menu)); - item.show(); - (item, menu) -} - -fn create_gtk_menu_item( - label: &str, - enabled: bool, - accelerator: &Option, - id: u64, - accel_group: >k::AccelGroup, -) -> gtk::MenuItem { - let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label)); - item.set_sensitive(enabled); - if let Some(accelerator) = accelerator { - register_accelerator(&item, accel_group, accelerator); - } - item.connect_activate(move |_| { - let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); - }); - item.show(); - item -} - -fn create_gtk_check_menu_item( - label: &str, - enabled: bool, - checked: bool, - accelerator: &Option, - id: u64, - accel_group: >k::AccelGroup, -) -> gtk::CheckMenuItem { - let item = gtk::CheckMenuItem::with_mnemonic(&to_gtk_menemenoic(label)); - item.set_sensitive(enabled); - item.set_active(checked); - if let Some(accelerator) = accelerator { - register_accelerator(&item, accel_group, accelerator); - } - item.connect_activate(move |_| { - let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); - }); - item.show(); - item -} - -impl NativeMenuItem { - fn add_to_gtk_menu>(&self, gtk_menu: &M) { - match self { - NativeMenuItem::Copy => { - let item = gtk::MenuItem::with_mnemonic("_Copy"); - let (key, modifiers) = gtk::accelerator_parse("X"); - item.child() - .unwrap() - .downcast::() - .unwrap() - .set_accel(key, modifiers); - item.connect_activate(move |_| { - // TODO: wayland - if let Ok(xdo) = libxdo::XDo::new(None) { - let _ = xdo.send_keysequence("ctrl+c", 0); - } - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::Cut => { - let item = gtk::MenuItem::with_mnemonic("Cu_t"); - let (key, modifiers) = gtk::accelerator_parse("X"); - item.child() - .unwrap() - .downcast::() - .unwrap() - .set_accel(key, modifiers); - item.connect_activate(move |_| { - // TODO: wayland - if let Ok(xdo) = libxdo::XDo::new(None) { - let _ = xdo.send_keysequence("ctrl+x", 0); - } - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::Paste => { - let item = gtk::MenuItem::with_mnemonic("_Paste"); - let (key, modifiers) = gtk::accelerator_parse("V"); - item.child() - .unwrap() - .downcast::() - .unwrap() - .set_accel(key, modifiers); - item.connect_activate(move |_| { - // TODO: wayland - if let Ok(xdo) = libxdo::XDo::new(None) { - let _ = xdo.send_keysequence("ctrl+v", 0); - } - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::SelectAll => { - let item = gtk::MenuItem::with_mnemonic("Select _All"); - let (key, modifiers) = gtk::accelerator_parse("A"); - item.child() - .unwrap() - .downcast::() - .unwrap() - .set_accel(key, modifiers); - item.connect_activate(move |_| { - // TODO: wayland - if let Ok(xdo) = libxdo::XDo::new(None) { - let _ = xdo.send_keysequence("ctrl+a", 0); - } - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::Separator => { - gtk_menu.append(>k::SeparatorMenuItem::new()); - } - NativeMenuItem::Minimize => { - let item = gtk::MenuItem::with_mnemonic("_Minimize"); - item.connect_activate(move |m| { - if let Some(window) = m.window() { - window.iconify() - } - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::CloseWindow => { - let item = gtk::MenuItem::with_mnemonic("C_lose Window"); - item.connect_activate(move |m| { - if let Some(window) = m.window() { - window.destroy() - } - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::Quit => { - let item = gtk::MenuItem::with_mnemonic("_Quit"); - item.connect_activate(move |_| { - std::process::exit(0); - }); - item.show(); - gtk_menu.append(&item); - } - NativeMenuItem::About(app_name, metadata) => { - let app_name = app_name.clone(); - let metadata = metadata.clone(); - let item = gtk::MenuItem::with_label(&format!("About {}", app_name)); - item.connect_activate(move |_| { - let mut builder = gtk::builders::AboutDialogBuilder::new() - .program_name(&app_name) - .modal(true) - .resizable(false); - - if let Some(version) = &metadata.version { - builder = builder.version(version); - } - if let Some(authors) = &metadata.authors { - builder = builder.authors(authors.clone()); - } - if let Some(comments) = &metadata.comments { - builder = builder.comments(comments); - } - if let Some(copyright) = &metadata.copyright { - builder = builder.copyright(copyright); - } - if let Some(license) = &metadata.license { - builder = builder.license(license); - } - if let Some(website) = &metadata.website { - builder = builder.website(website); - } - if let Some(website_label) = &metadata.website_label { - builder = builder.website_label(website_label); - } - let about = builder.build(); - about.run(); - unsafe { - about.destroy(); - } - }); - item.show(); - gtk_menu.append(&item); - } - _ => {} - } - } -} diff --git a/src/platform_impl/macos/accelerator.rs b/src/platform_impl/macos/accelerator.rs index c45e829..ffae65c 100644 --- a/src/platform_impl/macos/accelerator.rs +++ b/src/platform_impl/macos/accelerator.rs @@ -3,17 +3,11 @@ use keyboard_types::{Code, Modifiers}; use crate::accelerator::Accelerator; -/// Mnemonic is deprecated since macOS 10 -pub fn remove_mnemonic(string: impl AsRef) -> String { - string.as_ref().replace("&", "") -} - impl Accelerator { - /// Return the string value of this hotkey, for use with Cocoa `NSResponder` - /// objects. + /// Return the string value of this hotkey, without modifiers. /// /// Returns the empty string if no key equivalent is known. - pub fn key_equivalent(&self) -> String { + pub fn key_equivalent(self) -> String { match self.key { Code::KeyA => "a".into(), Code::KeyB => "b".into(), @@ -113,13 +107,14 @@ impl Accelerator { } } - pub fn key_modifier_mask(&self) -> NSEventModifierFlags { + /// Return the modifiers of this hotkey, as an NSEventModifierFlags bitflag. + pub fn key_modifier_mask(self) -> NSEventModifierFlags { let mods: Modifiers = self.mods; let mut flags = NSEventModifierFlags::empty(); if mods.contains(Modifiers::SHIFT) { flags.insert(NSEventModifierFlags::NSShiftKeyMask); } - if mods.contains(Modifiers::SUPER) { + if mods.contains(Modifiers::META) { flags.insert(NSEventModifierFlags::NSCommandKeyMask); } if mods.contains(Modifiers::ALT) { diff --git a/src/platform_impl/macos/menu_item.rs b/src/platform_impl/macos/menu_item.rs deleted file mode 100644 index 6988d82..0000000 --- a/src/platform_impl/macos/menu_item.rs +++ /dev/null @@ -1,271 +0,0 @@ -use crate::accelerator::Accelerator; -use crate::counter::Counter; -use crate::platform_impl::platform_impl::accelerator::remove_mnemonic; -use cocoa::{ - appkit::{NSButton, NSEventModifierFlags, NSMenuItem}, - base::{id, nil, BOOL, NO, YES}, - foundation::NSString, -}; -use objc::{ - class, - declare::ClassDecl, - msg_send, - runtime::{Class, Object, Sel}, - sel, sel_impl, -}; -use std::rc::Rc; -use std::sync::Once; - -static COUNTER: Counter = Counter::new(); - -#[derive(Debug, Clone)] -pub struct MenuItem { - pub(crate) id: u64, - pub(crate) ns_menu_item: id, - label: Rc, -} - -impl MenuItem { - pub fn new>( - label: S, - enabled: bool, - selector: Sel, - accelerator: Option, - ) -> Self { - let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); - - unsafe { - (&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id); - let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item]; - - if !enabled { - let () = msg_send![ns_menu_item, setEnabled: NO]; - } - } - Self { - id, - ns_menu_item, - label: Rc::from(label.as_ref()), - } - } - - pub fn label(&self) -> String { - self.label.to_string() - } - - pub fn set_label>(&mut self, label: S) { - unsafe { - 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 { - unsafe { - let enabled: BOOL = msg_send![self.ns_menu_item, isEnabled]; - enabled == YES - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - unsafe { - let status = match enabled { - true => YES, - false => NO, - }; - let () = msg_send![self.ns_menu_item, setEnabled: status]; - } - } - - pub fn id(&self) -> u64 { - self.id - } -} - -#[derive(Debug, Clone)] -pub struct CheckMenuItem { - pub(crate) id: u64, - pub(crate) ns_menu_item: id, - label: Rc, -} - -impl CheckMenuItem { - pub fn new>( - label: S, - enabled: bool, - checked: bool, - selector: Sel, - accelerator: Option, - ) -> Self { - let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); - - unsafe { - (&mut *ns_menu_item).set_ivar(MENU_IDENTITY, id); - let () = msg_send![&*ns_menu_item, setTarget:&*ns_menu_item]; - - if !enabled { - let () = msg_send![ns_menu_item, setEnabled: NO]; - } - - if checked { - let () = msg_send![ns_menu_item, setState: 1_isize]; - } - } - Self { - id, - ns_menu_item, - label: Rc::from(label.as_ref()), - } - } - - pub fn label(&self) -> String { - self.label.to_string() - } - - pub fn set_label>(&mut self, label: S) { - unsafe { - 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 { - unsafe { - let enabled: BOOL = msg_send![self.ns_menu_item, isEnabled]; - enabled == YES - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - unsafe { - let status = match enabled { - true => YES, - false => NO, - }; - let () = msg_send![self.ns_menu_item, setEnabled: status]; - } - } - - pub fn checked(&self) -> bool { - unsafe { - let checked: isize = msg_send![self.ns_menu_item, state]; - checked == 1_isize - } - } - - pub fn set_checked(&mut self, checked: bool) { - unsafe { - let state = match checked { - true => 1_isize, - false => 0_isize, - }; - let () = msg_send![self.ns_menu_item, setState: state]; - } - } - - pub fn id(&self) -> u64 { - self.id - } -} - -pub fn make_menu_item( - title: &str, - selector: Sel, - accelerator: Option, -) -> (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, accelerator); - - (menu_id, menu_item) - } -} - -fn make_menu_item_alloc() -> *mut Object { - unsafe { msg_send![make_menu_item_class(), alloc] } -} - -static MENU_IDENTITY: &str = "MenuItemIdentity"; - -fn make_menu_item_class() -> *const Class { - static mut APP_CLASS: *const Class = 0 as *const Class; - static INIT: Once = Once::new(); - - INIT.call_once(|| unsafe { - let superclass = class!(NSMenuItem); - let mut decl = ClassDecl::new("MenuItem", superclass).unwrap(); - decl.add_ivar::(MENU_IDENTITY); - - decl.add_method( - sel!(dealloc), - dealloc_custom_menuitem as extern "C" fn(&Object, _), - ); - - decl.add_method( - sel!(fireMenubarAction:), - fire_menu_bar_click as extern "C" fn(&Object, _, id), - ); - - decl.add_method( - sel!(fireStatusbarAction:), - fire_status_bar_click as extern "C" fn(&Object, _, id), - ); - - APP_CLASS = decl.register(); - }); - - unsafe { APP_CLASS } -} - -fn make_menu_item_from_alloc( - alloc: *mut Object, - title: *mut Object, - selector: Sel, - accelerator: Option, -) -> *mut Object { - unsafe { - let (key_equivalent, masks) = match accelerator { - Some(accelerator) => { - let key = accelerator.key_equivalent(); - let mods = accelerator.key_modifier_mask(); - 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_equivalent]; - item.setKeyEquivalentModifierMask_(masks); - - item - } -} - -extern "C" fn fire_menu_bar_click(this: &Object, _: Sel, _item: id) { - send_event(this); -} - -extern "C" fn fire_status_bar_click(this: &Object, _: Sel, _item: id) { - send_event(this); -} - -extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { - unsafe { - let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; - } -} - -fn send_event(this: &Object) { - let id: u64 = unsafe { *this.get_ivar(MENU_IDENTITY) }; - let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id: id as _ }); -} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 000282b..463de4a 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,212 +1,846 @@ mod accelerator; -mod menu_item; +mod util; + +use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Once}; -use crate::accelerator::{RawMods, SysMods}; -use crate::NativeMenuItem; -use crate::{accelerator::Accelerator, platform_impl::platform_impl::menu_item::make_menu_item}; use cocoa::{ - appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, - base::{id, nil, selector, NO}, - foundation::{NSAutoreleasePool, NSString}, + appkit::{CGFloat, NSApp, NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem}, + base::{id, nil, selector, NO, YES}, + foundation::{NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSString}, +}; +use objc::{ + declare::ClassDecl, + runtime::{Class, Object, Sel}, }; -use keyboard_types::Code; -use objc::{class, msg_send, sel, sel_impl}; -use self::accelerator::remove_mnemonic; -pub use menu_item::CheckMenuItem; -pub use menu_item::MenuItem; +use self::util::{app_name_string, strip_mnemonic}; +use crate::{ + accelerator::Accelerator, + predefined::PredfinedMenuItemType, + util::{AddOp, Counter}, + MenuItemExt, MenuItemType, +}; -#[derive(Debug, Clone)] -pub struct Menu(id); +static COUNTER: Counter = Counter::new(); +static BLOCK_PTR: &str = "mudaMenuItemBlockPtr"; + +/// A generic child in a menu +/// +/// Be careful when cloning this item and treat it as read-only +#[derive(Debug, Default, PartialEq, Eq)] +#[allow(dead_code)] +struct MenuChild { + // shared fields between submenus and menu items + type_: MenuItemType, + id: u32, + text: String, + enabled: bool, + + ns_menu_items: HashMap>, + + // menu item fields + accelerator: Option, + + // predefined menu item fields + predefined_item_type: PredfinedMenuItemType, + + // check menu item fields + checked: bool, + + // submenu fields + children: Option>>>, + ns_menus: HashMap>, +} + +impl MenuChild { + pub fn id(&self) -> u32 { + self.id + } + + pub fn text(&self) -> String { + self.text.clone() + } + + pub fn set_text(&mut self, text: &str) { + self.text = strip_mnemonic(text); + unsafe { + let title = NSString::alloc(nil).init_str(&self.text).autorelease(); + for ns_items in self.ns_menu_items.values() { + for &ns_item in ns_items { + let () = msg_send![ns_item, setTitle: title]; + let ns_submenu: *mut Object = msg_send![ns_item, submenu]; + if ns_submenu != nil { + let () = msg_send![ns_submenu, setTitle: title]; + } + } + } + } + } + + pub fn is_enabled(&self) -> bool { + self.enabled + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + for ns_items in self.ns_menu_items.values() { + for &ns_item in ns_items { + unsafe { + let () = msg_send![ns_item, setEnabled: if enabled { YES } else { NO }]; + } + } + } + } + + pub fn is_checked(&self) -> bool { + self.checked + } + + pub fn set_checked(&mut self, checked: bool) { + self.checked = checked; + for ns_items in self.ns_menu_items.values() { + for &ns_item in ns_items { + unsafe { + let () = msg_send![ns_item, setState: checked as u32]; + } + } + } + } +} + +#[derive(Clone, Debug)] +pub struct Menu { + id: u32, + ns_menu: id, + children: Rc>>>>, +} impl Menu { pub fn new() -> Self { - unsafe { - let ns_menu = NSMenu::alloc(nil).autorelease(); - let () = msg_send![ns_menu, setAutoenablesItems: NO]; - Self(ns_menu) + Self { + id: COUNTER.next(), + ns_menu: unsafe { + let ns_menu = NSMenu::alloc(nil).autorelease(); + ns_menu.setAutoenablesItems(NO); + ns_menu + }, + children: Rc::new(RefCell::new(Vec::new())), } } - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - let menu = Menu::new(); - let menu_item = MenuItem::new("", enabled, sel!(fireMenubarAction:), None); + pub fn append(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Append) + } + + pub fn prepend(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Insert(0)) + } + + pub fn insert(&self, item: &dyn crate::MenuItemExt, position: usize) { + self.add_menu_item(item, AddOp::Insert(position)) + } + + fn add_menu_item(&self, item: &dyn crate::MenuItemExt, op: AddOp) { + let ns_menu_item: *mut Object = item.make_ns_item_for_menu(self.id); + let child: Rc> = item.get_child(); unsafe { - menu_item.ns_menu_item.setSubmenu_(menu.0); - self.0.addItem_(menu_item.ns_menu_item); + match op { + AddOp::Append => { + self.ns_menu.addItem_(ns_menu_item); + self.children.borrow_mut().push(child); + } + AddOp::Insert(position) => { + let () = msg_send![self.ns_menu, insertItem: ns_menu_item atIndex: position as NSInteger]; + self.children.borrow_mut().insert(position, child); + } + } + } + } + + pub fn remove(&self, item: &dyn crate::MenuItemExt) -> crate::Result<()> { + // get a list of instances of the specified NSMenuItem in this menu + if let Some(ns_menu_items) = match item.type_() { + MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + submenu.0 .0.borrow_mut() + } + MenuItemType::Normal => { + let menuitem = item.as_any().downcast_ref::().unwrap(); + menuitem.0 .0.borrow_mut() + } + MenuItemType::Check => { + let menuitem = item + .as_any() + .downcast_ref::() + .unwrap(); + menuitem.0 .0.borrow_mut() + } + MenuItemType::Predefined => { + let menuitem = item + .as_any() + .downcast_ref::() + .unwrap(); + menuitem.0 .0.borrow_mut() + } + } + .ns_menu_items + .remove(&self.id) + { + // remove each NSMenuItem from the NSMenu + unsafe { + for item in ns_menu_items { + let () = msg_send![self.ns_menu, removeItem: item]; + } + } } - let mut sub_menu = Submenu { menu, menu_item }; - sub_menu.set_label(label); + // remove the item from our internal list of children + let mut children = self.children.borrow_mut(); + let index = children + .iter() + .position(|e| e.borrow().id() == item.id()) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + children.remove(index); - sub_menu + Ok(()) + } + + pub fn items(&self) -> Vec> { + self.children + .borrow() + .iter() + .map(|c| -> Box { + let child = c.borrow(); + match child.type_ { + MenuItemType::Submenu => Box::new(crate::Submenu(Submenu(c.clone()))), + MenuItemType::Normal => Box::new(crate::MenuItem(MenuItem(c.clone()))), + MenuItemType::Predefined => { + Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) + } + MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), + } + }) + .collect() } pub fn init_for_nsapp(&self) { - unsafe { - NSApp().setMainMenu_(self.0); - } + unsafe { NSApp().setMainMenu_(self.ns_menu) } } pub fn remove_for_nsapp(&self) { + unsafe { NSApp().setMainMenu_(nil) } + } + + pub fn show_context_menu_for_nsview(&self, view: id, x: f64, y: f64) { unsafe { - NSApp().setMainMenu_(std::ptr::null_mut()); + let window: id = msg_send![view, window]; + let scale_factor: CGFloat = msg_send![window, backingScaleFactor]; + let view_point = NSPoint::new(x / scale_factor, y / scale_factor); + let view_rect: NSRect = msg_send![view, frame]; + let location = NSPoint::new(view_point.x, view_rect.size.height - view_point.y); + msg_send![self.ns_menu, popUpMenuPositioningItem: nil atLocation: location inView: view] } } } -#[derive(Debug, Clone)] -pub struct Submenu { - pub(crate) menu: Menu, - pub(crate) menu_item: MenuItem, -} +#[derive(Clone)] +pub(crate) struct Submenu(Rc>); impl Submenu { - pub fn label(&self) -> String { - self.menu_item.label() - } - - pub fn set_label>(&mut self, label: S) { - let label = remove_mnemonic(label); - self.menu_item.set_label(&label); - unsafe { - let menu_title = NSString::alloc(nil).init_str(&label); - let () = msg_send![self.menu.0, setTitle: menu_title]; - } - } - - pub fn enabled(&self) -> bool { - self.menu_item.enabled() - } - - pub fn set_enabled(&mut self, _enabled: bool) { - self.menu_item.set_enabled(_enabled) - } - - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - self.menu.add_submenu(label, enabled) - } - - pub fn add_item>( - &mut self, - label: S, - enabled: bool, - accelerator: Option, - ) -> MenuItem { - let item = MenuItem::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) { - let (_, native_menu_item) = match item { - NativeMenuItem::Separator => unsafe { (0, NSMenuItem::separatorItem(nil)) }, - NativeMenuItem::About(app_name, _) => { - let title = format!("About {}", app_name); - make_menu_item( - title.as_str(), - selector("orderFrontStandardAboutPanel:"), - None, - ) - } - NativeMenuItem::CloseWindow => make_menu_item( - "Close Window", - selector("performClose:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyW)), - ), - NativeMenuItem::Quit => make_menu_item( - "Quit", - selector("terminate:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyQ)), - ), - NativeMenuItem::Hide => make_menu_item( - "Hide", - selector("hide:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyH)), - ), - NativeMenuItem::HideOthers => make_menu_item( - "Hide Others", - selector("hideOtherApplications:"), - Some(Accelerator::new(RawMods::Alt, Code::KeyH)), - ), - NativeMenuItem::ShowAll => { - make_menu_item("Show All", selector("unhideAllApplications:"), None) - } - NativeMenuItem::ToggleFullScreen => make_menu_item( - "Toggle Full Screen", - selector("toggleFullScreen:"), - Some(Accelerator::new(RawMods::Ctrl, Code::KeyF)), - ), - NativeMenuItem::Minimize => make_menu_item( - "Minimize", - selector("performMiniaturize:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyM)), - ), - NativeMenuItem::Zoom => make_menu_item("Zoom", selector("performZoom:"), None), - NativeMenuItem::Copy => make_menu_item( - "Copy", - selector("copy:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyC)), - ), - NativeMenuItem::Cut => make_menu_item( - "Cut", - selector("cut:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyX)), - ), - NativeMenuItem::Paste => make_menu_item( - "Paste", - selector("paste:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyV)), - ), - NativeMenuItem::Undo => make_menu_item( - "Undo", - selector("undo:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyZ)), - ), - NativeMenuItem::Redo => make_menu_item( - "Redo", - selector("redo:"), - Some(Accelerator::new(SysMods::CmdShift, Code::KeyZ)), - ), - NativeMenuItem::SelectAll => make_menu_item( - "Select All", - selector("selectAll:"), - Some(Accelerator::new(SysMods::Cmd, Code::KeyA)), - ), - NativeMenuItem::Services => unsafe { - let (_, item) = make_menu_item("Services", sel!(fireMenubarAction:), None); - let app_class = class!(NSApplication); - let app: id = msg_send![app_class, sharedApplication]; - let services: id = msg_send![app, servicesMenu]; - let _: () = msg_send![&*item, setSubmenu: services]; - (0, item) - }, - }; - unsafe { - self.menu.0.addItem_(native_menu_item); - } - } - - pub fn add_check_item>( - &mut self, - label: S, - enabled: bool, - checked: bool, - accelerator: Option, - ) -> CheckMenuItem { - let item = CheckMenuItem::new( - label, + pub fn new(text: &str, enabled: bool) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Submenu, + text: strip_mnemonic(text), enabled, - checked, - sel!(fireMenubarAction:), - accelerator, - ); + children: Some(Vec::new()), + ..Default::default() + }))) + } + + pub fn id(&self) -> u32 { + self.0.borrow().id() + } + + pub fn make_ns_item_for_menu(&self, menu_id: u32) -> id { + let mut self_ = self.0.borrow_mut(); + let ns_menu_item: *mut Object; + let ns_submenu: *mut Object; + unsafe { - self.menu.0.addItem_(item.ns_menu_item); + ns_menu_item = NSMenuItem::alloc(nil).autorelease(); + ns_submenu = NSMenu::alloc(nil).autorelease(); + + let title = NSString::alloc(nil).init_str(&self_.text).autorelease(); + let () = msg_send![ns_submenu, setTitle: title]; + let () = msg_send![ns_menu_item, setTitle: title]; + let () = msg_send![ns_menu_item, setSubmenu: ns_submenu]; + + if !self_.enabled { + let () = msg_send![ns_menu_item, setEnabled: NO]; + } + } + + for item in self_.children.as_ref().unwrap() { + let item_type = &item.borrow().type_.clone(); + let ns_item = match item_type { + MenuItemType::Submenu => Submenu(item.clone()).make_ns_item_for_menu(menu_id), + MenuItemType::Normal => MenuItem(item.clone()).make_ns_item_for_menu(menu_id), + MenuItemType::Predefined => { + PredefinedMenuItem(item.clone()).make_ns_item_for_menu(menu_id) + } + MenuItemType::Check => CheckMenuItem(item.clone()).make_ns_item_for_menu(menu_id), + }; + unsafe { ns_submenu.addItem_(ns_item) }; + } + + self_ + .ns_menus + .entry(menu_id) + .or_insert_with(Vec::new) + .push(ns_submenu); + + self_ + .ns_menu_items + .entry(menu_id) + .or_insert_with(Vec::new) + .push(ns_menu_item); + + ns_menu_item + } + + pub fn append(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Append) + } + + pub fn prepend(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Insert(0)) + } + + pub fn insert(&self, item: &dyn crate::MenuItemExt, position: usize) { + self.add_menu_item(item, AddOp::Insert(position)) + } + + fn add_menu_item(&self, item: &dyn crate::MenuItemExt, op: AddOp) { + let mut self_ = self.0.borrow_mut(); + + let item_child: Rc> = item.get_child(); + + unsafe { + match op { + AddOp::Append => { + for menus in self_.ns_menus.values() { + for ns_menu in menus { + let ns_menu_item: *mut Object = item.make_ns_item_for_menu(self_.id); + ns_menu.addItem_(ns_menu_item); + } + } + self_.children.as_mut().unwrap().push(item_child); + } + AddOp::Insert(position) => { + for menus in self_.ns_menus.values() { + for &ns_menu in menus { + let ns_menu_item: *mut Object = item.make_ns_item_for_menu(self_.id); + let () = msg_send![ns_menu, insertItem: ns_menu_item atIndex: position as NSInteger]; + } + } + self_ + .children + .as_mut() + .unwrap() + .insert(position, item_child); + } + } + } + } + + pub fn remove(&self, item: &dyn crate::MenuItemExt) -> crate::Result<()> { + let mut child = self.0.borrow_mut(); + + let item_child: Rc> = item.get_child(); + + // get a list of instances of the specified NSMenuItem in this menu + if let Some(ns_menu_items) = item_child.borrow_mut().ns_menu_items.remove(&child.id) { + // remove each NSMenuItem from the NSMenu + unsafe { + for item in ns_menu_items { + for menus in child.ns_menus.values() { + for &ns_menu in menus { + let () = msg_send![ns_menu, removeItem: item]; + } + } + } + } + } + + // remove the item from our internal list of children + let children = child.children.as_mut().unwrap(); + let index = children + .iter() + .position(|e| e == &item_child) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + children.remove(index); + + Ok(()) + } + + pub fn items(&self) -> Vec> { + self.0 + .borrow() + .children + .as_ref() + .unwrap() + .iter() + .map(|c| -> Box { + let child = c.borrow(); + match child.type_ { + MenuItemType::Submenu => Box::new(crate::Submenu(Submenu(c.clone()))), + MenuItemType::Normal => Box::new(crate::MenuItem(MenuItem(c.clone()))), + MenuItemType::Predefined => { + Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) + } + MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), + } + }) + .collect() + } + + pub fn text(&self) -> String { + self.0.borrow().text() + } + + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) + } + + pub fn is_enabled(&self) -> bool { + self.0.borrow().is_enabled() + } + + pub fn set_enabled(&self, enabled: bool) { + self.0.borrow_mut().set_enabled(enabled) + } + + pub fn show_context_menu_for_nsview(&self, view: id, x: f64, y: f64) { + // TODO: this needs to work even if it hasn't already been added to a menu + if let Some(ns_menus) = self.0.borrow().ns_menus.get(&1) { + unsafe { + let window: id = msg_send![view, window]; + let scale_factor: CGFloat = msg_send![window, backingScaleFactor]; + let view_point = NSPoint::new(x / scale_factor, y / scale_factor); + let view_rect: NSRect = msg_send![view, frame]; + let location = NSPoint::new(view_point.x, view_rect.size.height - view_point.y); + msg_send![ns_menus[0], popUpMenuPositioningItem: nil atLocation: location inView: view] + } + } + } + + pub fn set_windows_menu_for_nsapp(&self) { + if let Some(ns_menus) = self.0.borrow().ns_menus.get(&1) { + unsafe { NSApp().setWindowsMenu_(ns_menus[0]) } + } + } + + pub fn set_help_menu_for_nsapp(&self) { + if let Some(ns_menus) = self.0.borrow().ns_menus.get(&1) { + unsafe { msg_send![NSApp(), setHelpMenu: ns_menus[0]] } } - item + } +} + +#[derive(Clone, Debug)] +pub(crate) struct MenuItem(Rc>); + +impl MenuItem { + pub fn new(text: &str, enabled: bool, accelerator: Option) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Normal, + text: strip_mnemonic(text), + enabled, + id: COUNTER.next(), + accelerator, + ..Default::default() + }))) + } + + pub fn make_ns_item_for_menu(&self, menu_id: u32) -> id { + let mut child = self.0.borrow_mut(); + + let ns_menu_item = create_ns_menu_item( + &child.text, + Some(sel!(fireMenuItemAction:)), + &child.accelerator, + ); + + unsafe { + let _: () = msg_send![ns_menu_item, setTarget: ns_menu_item]; + let _: () = msg_send![ns_menu_item, setTag:child.id()]; + + // Store a raw pointer to the `MenuChild` as an instance variable on the native menu item + let ptr = Box::into_raw(Box::new(&*child)); + (*ns_menu_item).set_ivar(BLOCK_PTR, ptr as usize); + + if !child.enabled { + let () = msg_send![ns_menu_item, setEnabled: NO]; + } + } + + child + .ns_menu_items + .entry(menu_id) + .or_insert_with(Vec::new) + .push(ns_menu_item); + + ns_menu_item + } + + pub fn id(&self) -> u32 { + self.0.borrow().id() + } + + pub fn text(&self) -> String { + self.0.borrow().text() + } + + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) + } + + pub fn is_enabled(&self) -> bool { + self.0.borrow().is_enabled() + } + + pub fn set_enabled(&self, enabled: bool) { + self.0.borrow_mut().set_enabled(enabled) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PredefinedMenuItem(Rc>); + +impl PredefinedMenuItem { + pub fn new(item_type: PredfinedMenuItemType, text: Option) -> Self { + let text = strip_mnemonic(text.unwrap_or_else(|| { + match item_type { + PredfinedMenuItemType::About(_) => { + format!("About {}", unsafe { app_name_string() }.unwrap_or_default()) + .trim() + .to_string() + } + PredfinedMenuItemType::Hide => { + format!("Hide {}", unsafe { app_name_string() }.unwrap_or_default()) + .trim() + .to_string() + } + PredfinedMenuItemType::Quit => { + format!("Quit {}", unsafe { app_name_string() }.unwrap_or_default()) + .trim() + .to_string() + } + _ => item_type.text().to_string(), + } + })); + let accelerator = item_type.accelerator(); + + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Predefined, + text, + enabled: true, + id: COUNTER.next(), + accelerator, + predefined_item_type: item_type, + // ns_menu_item, + ..Default::default() + }))) + } + + pub fn make_ns_item_for_menu(&self, menu_id: u32) -> id { + let mut child = self.0.borrow_mut(); + + let item_type = &child.predefined_item_type; + let ns_menu_item = match item_type { + PredfinedMenuItemType::Separator => unsafe { + NSMenuItem::separatorItem(nil).autorelease() + }, + _ => create_ns_menu_item(&child.text, item_type.selector(), &child.accelerator), + }; + + unsafe { + if !child.enabled { + let () = msg_send![ns_menu_item, setEnabled: NO]; + } + if child.predefined_item_type == PredfinedMenuItemType::Services { + // we have to assign an empty menu as the app's services menu, and macOS will populate it + let services_menu = NSMenu::new(nil).autorelease(); + let () = msg_send![NSApp(), setServicesMenu: services_menu]; + let () = msg_send![ns_menu_item, setSubmenu: services_menu]; + } + } + + child + .ns_menu_items + .entry(menu_id) + .or_insert_with(Vec::new) + .push(ns_menu_item); + + ns_menu_item + } + + pub fn id(&self) -> u32 { + self.0.borrow().id() + } + + pub fn text(&self) -> String { + self.0.borrow().text() + } + + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct CheckMenuItem(Rc>); + +impl CheckMenuItem { + pub fn new(text: &str, enabled: bool, checked: bool, accelerator: Option) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Check, + text: text.to_string(), + enabled, + id: COUNTER.next(), + accelerator, + checked, + ..Default::default() + }))) + } + + pub fn make_ns_item_for_menu(&self, menu_id: u32) -> id { + let mut child = self.0.borrow_mut(); + + let ns_menu_item = create_ns_menu_item( + &child.text, + Some(sel!(fireMenuItemAction:)), + &child.accelerator, + ); + + unsafe { + let _: () = msg_send![ns_menu_item, setTarget: ns_menu_item]; + let _: () = msg_send![ns_menu_item, setTag:child.id()]; + + // Store a raw pointer to the `MenuChild` as an instance variable on the native menu item + let ptr = Box::into_raw(Box::new(&*child)); + (*ns_menu_item).set_ivar(BLOCK_PTR, ptr as usize); + + if !child.enabled { + let () = msg_send![ns_menu_item, setEnabled: NO]; + } + if child.checked { + let () = msg_send![ns_menu_item, setState: 1_isize]; + } + } + + child + .ns_menu_items + .entry(menu_id) + .or_insert_with(Vec::new) + .push(ns_menu_item); + + ns_menu_item + } + + pub fn id(&self) -> u32 { + self.0.borrow().id() + } + + pub fn text(&self) -> String { + self.0.borrow().text() + } + + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) + } + + pub fn is_enabled(&self) -> bool { + self.0.borrow().is_enabled() + } + + pub fn set_enabled(&self, enabled: bool) { + self.0.borrow_mut().set_enabled(enabled) + } + + pub fn is_checked(&self) -> bool { + self.0.borrow().is_checked() + } + + pub fn set_checked(&self, checked: bool) { + self.0.borrow_mut().set_checked(checked) + } +} + +impl PredfinedMenuItemType { + pub(crate) fn selector(&self) -> Option { + match self { + PredfinedMenuItemType::Separator => None, + PredfinedMenuItemType::Copy => Some(selector("copy:")), + PredfinedMenuItemType::Cut => Some(selector("cut:")), + PredfinedMenuItemType::Paste => Some(selector("paste:")), + PredfinedMenuItemType::SelectAll => Some(selector("selectAll:")), + PredfinedMenuItemType::Undo => Some(selector("undo:")), + PredfinedMenuItemType::Redo => Some(selector("redo:")), + PredfinedMenuItemType::Minimize => Some(selector("performMiniaturize:")), + PredfinedMenuItemType::Maximize => Some(selector("performZoom:")), + PredfinedMenuItemType::Fullscreen => Some(selector("toggleFullScreen:")), + PredfinedMenuItemType::Hide => Some(selector("hide:")), + PredfinedMenuItemType::HideOthers => Some(selector("hideOtherApplications:")), + PredfinedMenuItemType::ShowAll => Some(selector("unhideAllApplications:")), + PredfinedMenuItemType::CloseWindow => Some(selector("performClose:")), + PredfinedMenuItemType::Quit => Some(selector("terminate:")), + PredfinedMenuItemType::About(_) => Some(selector("orderFrontStandardAboutPanel:")), + PredfinedMenuItemType::Services => None, + PredfinedMenuItemType::None => None, + } + } +} + +impl dyn MenuItemExt + '_ { + fn get_child(&self) -> Rc> { + match self.type_() { + MenuItemType::Submenu => { + let submenu = self.as_any().downcast_ref::().unwrap(); + Rc::clone(&submenu.0 .0) + } + MenuItemType::Normal => { + let menuitem = self.as_any().downcast_ref::().unwrap(); + Rc::clone(&menuitem.0 .0) + } + MenuItemType::Check => { + let menuitem = self + .as_any() + .downcast_ref::() + .unwrap(); + Rc::clone(&menuitem.0 .0) + } + MenuItemType::Predefined => { + let menuitem = self + .as_any() + .downcast_ref::() + .unwrap(); + Rc::clone(&menuitem.0 .0) + } + } + } + + fn make_ns_item_for_menu(&self, menu_id: u32) -> *mut Object { + match self.type_() { + MenuItemType::Submenu => { + let submenu = self.as_any().downcast_ref::().unwrap(); + submenu.0.make_ns_item_for_menu(menu_id) + } + MenuItemType::Normal => { + let menuitem = self.as_any().downcast_ref::().unwrap(); + menuitem.0.make_ns_item_for_menu(menu_id) + } + MenuItemType::Check => { + let menuitem = self + .as_any() + .downcast_ref::() + .unwrap(); + menuitem.0.make_ns_item_for_menu(menu_id) + } + MenuItemType::Predefined => { + let menuitem = self + .as_any() + .downcast_ref::() + .unwrap(); + menuitem.0.make_ns_item_for_menu(menu_id) + } + } + } +} + +fn make_menu_item_class() -> *const Class { + static mut APP_CLASS: *const Class = 0 as *const Class; + static INIT: Once = Once::new(); + + // The first time the function is called, + INIT.call_once(|| unsafe { + let superclass = class!(NSMenuItem); + let mut decl = ClassDecl::new("MudaMenuItem", superclass).unwrap(); + + // An instance variable which will hold a pointer to the `MenuChild` + decl.add_ivar::(BLOCK_PTR); + + decl.add_method( + sel!(dealloc), + dealloc_custom_menuitem as extern "C" fn(&Object, _), + ); + + decl.add_method( + sel!(fireMenuItemAction:), + fire_menu_item_click as extern "C" fn(&Object, _, id), + ); + + APP_CLASS = decl.register(); + }); + + unsafe { APP_CLASS } +} + +extern "C" fn dealloc_custom_menuitem(this: &Object, _: Sel) { + unsafe { + let ptr: usize = *this.get_ivar(BLOCK_PTR); + let obj = ptr as *mut &mut MenuChild; + drop(Box::from_raw(obj)); + let _: () = msg_send![super(this, class!(NSMenuItem)), dealloc]; + } +} + +extern "C" fn fire_menu_item_click(this: &Object, _: Sel, _item: id) { + unsafe { + let id: u32 = msg_send![this, tag]; + + // Create a reference to the `MenuChild` from the raw pointer + // stored as an instance variable on the native menu item + let ptr: usize = *this.get_ivar(BLOCK_PTR); + let item = ptr as *mut &mut MenuChild; + + if (*item).type_ == MenuItemType::Check { + (*item).set_checked(!(*item).is_checked()); + } + + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); + } +} + +fn create_ns_menu_item( + title: &str, + selector: Option, + accelerator: &Option, +) -> id { + unsafe { + let title = NSString::alloc(nil).init_str(title).autorelease(); + + let selector = selector.unwrap_or_else(|| Sel::from_ptr(std::ptr::null())); + + let key_equivalent = accelerator + .clone() + .map(|accel| accel.key_equivalent()) + .unwrap_or_default(); + let key_equivalent = NSString::alloc(nil) + .init_str(key_equivalent.as_str()) + .autorelease(); + + let modifier_mask = accelerator + .clone() + .map(|accel| accel.key_modifier_mask()) + .unwrap_or_else(NSEventModifierFlags::empty); + + let ns_menu_item: *mut Object = msg_send![make_menu_item_class(), alloc]; + + ns_menu_item.initWithTitle_action_keyEquivalent_(title, selector, key_equivalent); + ns_menu_item.setKeyEquivalentModifierMask_(modifier_mask); + + ns_menu_item.autorelease() } } diff --git a/src/platform_impl/macos/util.rs b/src/platform_impl/macos/util.rs new file mode 100644 index 0000000..968326d --- /dev/null +++ b/src/platform_impl/macos/util.rs @@ -0,0 +1,41 @@ +use std::{slice, str}; + +use cocoa::{ + base::{id, nil}, + foundation::NSString, +}; + +/// Strips single `&` characters from the string. +/// +/// `&` can be escaped as `&&` to prevent stripping, in which case a single `&` will be output. +pub fn strip_mnemonic>(string: S) -> String { + string + .as_ref() + .replace("&&", "[~~]") + .replace('&', "") + .replace("[~~]", "&") +} + +/// Copies the contents of the NSString into a `String` which gets returned. +pub(crate) unsafe fn ns_string_to_rust(ns_string: id) -> String { + let slice = slice::from_raw_parts(ns_string.UTF8String() as *mut u8, ns_string.len()); + let string = str::from_utf8_unchecked(slice); + string.to_owned() +} + +/// Gets the app's name from the `localizedName` property of `NSRunningApplication`. +pub(crate) unsafe fn app_name() -> Option { + let app_class = class!(NSRunningApplication); + let app: id = msg_send![app_class, currentApplication]; + let app_name: id = msg_send![app, localizedName]; + if app_name != nil { + Some(app_name) + } else { + None + } +} + +/// Gets the app's name as a `String` from the `localizedName` property of `NSRunningApplication`. +pub(crate) unsafe fn app_name_string() -> Option { + app_name().map(|name| ns_string_to_rust(name)) +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index baf3dd8..4825721 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -1,11 +1,11 @@ -pub use self::platform_impl::*; - #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] -mod platform_impl; +mod platform; #[cfg(target_os = "linux")] -#[path = "linux/mod.rs"] -mod platform_impl; +#[path = "gtk/mod.rs"] +mod platform; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] -mod platform_impl; +mod platform; + +pub(crate) use self::platform::*; diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 7aa4c38..20f5b3c 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -1,481 +1,1032 @@ -#![cfg(target_os = "windows")] - mod accelerator; mod util; -use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem}; -use once_cell::sync::Lazy; -use std::{cell::RefCell, collections::HashMap, rc::Rc}; -use util::{decode_wide, encode_wide, LOWORD}; +use crate::{ + accelerator::Accelerator, + predefined::PredfinedMenuItemType, + util::{AddOp, Counter}, + MenuItemType, +}; +use std::{cell::RefCell, fmt::Debug, rc::Rc}; +use util::{decode_wide, encode_wide, Accel}; use windows_sys::Win32::{ - Foundation::{HWND, LPARAM, LRESULT, WPARAM}, + Foundation::{HWND, LPARAM, LRESULT, POINT, WPARAM}, + Graphics::Gdi::ClientToScreen, UI::{ Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VK_CONTROL}, Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass}, WindowsAndMessaging::{ - AppendMenuW, CloseWindow, CreateAcceleratorTableW, CreateMenu, DrawMenuBar, - EnableMenuItem, GetMenuItemInfoW, MessageBoxW, PostQuitMessage, SetMenu, - SetMenuItemInfoW, ShowWindow, ACCEL, HACCEL, HMENU, MB_ICONINFORMATION, MENUITEMINFOW, - MFS_CHECKED, MFS_DISABLED, MF_CHECKED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, - MF_SEPARATOR, MF_STRING, MF_UNCHECKED, MIIM_STATE, MIIM_STRING, SW_MINIMIZE, - WM_COMMAND, + AppendMenuW, CreateAcceleratorTableW, CreateMenu, CreatePopupMenu, + DestroyAcceleratorTable, DestroyWindow, DrawMenuBar, EnableMenuItem, GetMenuItemInfoW, + InsertMenuW, MessageBoxW, PostQuitMessage, RemoveMenu, SendMessageW, SetMenu, + SetMenuItemInfoW, ShowWindow, TrackPopupMenu, HACCEL, HMENU, MB_ICONINFORMATION, + MENUITEMINFOW, MFS_CHECKED, MFS_DISABLED, MF_BYCOMMAND, MF_BYPOSITION, MF_CHECKED, + MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MF_SEPARATOR, MF_STRING, MF_UNCHECKED, + MIIM_STATE, MIIM_STRING, SW_MINIMIZE, TPM_LEFTALIGN, WM_COMMAND, WM_DESTROY, }, }, }; -const COUNTER_START: u64 = 1000; +const COUNTER_START: u32 = 1000; static COUNTER: Counter = Counter::new_with_start(COUNTER_START); -struct InnerMenu { +type AccelWrapper = (HACCEL, Vec); + +/// A generic child in a menu +/// +/// Be careful when cloning this item and treat it as read-only +#[derive(Debug, Default)] +struct MenuChild { + // shared fields between submenus and menu items + type_: MenuItemType, + text: String, + enabled: bool, + parents_hemnu: Vec, + + // menu item fields + id: u32, + accelerator: Option, + + // predefined menu item fields + predefined_item_type: PredfinedMenuItemType, + + // check menu item fields + checked: bool, + + // submenu fields hmenu: HMENU, - accelerators: Vec, - haccel: HACCEL, + hpopupmenu: HMENU, + children: Option>>>, + root_menu_haccel: Option>>>, +} + +impl MenuChild { + fn id(&self) -> u32 { + match self.type_ { + MenuItemType::Submenu => self.hmenu as u32, + _ => self.id, + } + } + fn text(&self) -> String { + self.parents_hemnu + .first() + .map(|hmenu| { + let mut label = Vec::::new(); + + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as _; + info.fMask = MIIM_STRING; + info.dwTypeData = label.as_mut_ptr(); + + unsafe { GetMenuItemInfoW(*hmenu, self.id(), false.into(), &mut info) }; + + info.cch += 1; + info.dwTypeData = Vec::with_capacity(info.cch as usize).as_mut_ptr(); + + unsafe { GetMenuItemInfoW(*hmenu, self.id(), false.into(), &mut info) }; + + let text = decode_wide(info.dwTypeData); + text.split('\t').next().unwrap().to_string() + }) + .unwrap_or_else(|| self.text.clone()) + } + + fn set_text(&mut self, text: &str) { + self.text = text.to_string(); + for parent in &self.parents_hemnu { + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as _; + info.fMask = MIIM_STRING; + info.dwTypeData = encode_wide(text).as_mut_ptr(); + + unsafe { SetMenuItemInfoW(*parent, self.id(), false.into(), &info) }; + } + } + + fn is_enabled(&self) -> bool { + self.parents_hemnu + .first() + .map(|hmenu| { + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as _; + info.fMask = MIIM_STATE; + + unsafe { GetMenuItemInfoW(*hmenu, self.id(), false.into(), &mut info) }; + + (info.fState & MFS_DISABLED) == 0 + }) + .unwrap_or(self.enabled) + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + for parent in &self.parents_hemnu { + unsafe { + EnableMenuItem( + *parent, + self.id(), + if enabled { MF_ENABLED } else { MF_DISABLED }, + ) + }; + } + } + + fn is_checked(&self) -> bool { + self.parents_hemnu + .first() + .map(|hmenu| { + let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; + info.cbSize = std::mem::size_of::() as _; + info.fMask = MIIM_STATE; + + unsafe { GetMenuItemInfoW(*hmenu, self.id(), false.into(), &mut info) }; + + (info.fState & MFS_CHECKED) != 0 + }) + .unwrap_or(self.enabled) + } + + fn set_checked(&mut self, checked: bool) { + use windows_sys::Win32::UI::WindowsAndMessaging; + + self.checked = checked; + for parent in &self.parents_hemnu { + unsafe { + WindowsAndMessaging::CheckMenuItem( + *parent, + self.id(), + if checked { MF_CHECKED } else { MF_UNCHECKED }, + ) + }; + } + } } #[derive(Clone)] -pub struct Menu(Rc>); +pub(crate) struct Menu { + hmenu: HMENU, + hpopupmenu: HMENU, + hwnds: Rc>>, + haccel: Rc)>>, + children: Rc>>>>, +} impl Menu { pub fn new() -> Self { - Self(Rc::new(RefCell::new(InnerMenu { + Self { hmenu: unsafe { CreateMenu() }, - accelerators: Vec::new(), - haccel: 0, - }))) + hpopupmenu: unsafe { CreatePopupMenu() }, + haccel: Rc::new(RefCell::new((0, Vec::new()))), + children: Rc::new(RefCell::new(Vec::new())), + hwnds: Rc::new(RefCell::new(Vec::new())), + } } - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - let hmenu = unsafe { CreateMenu() }; - let mut flags = MF_POPUP; - if !enabled { - flags |= MF_GRAYED; + pub fn append(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Append) + } + + pub fn prepend(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Insert(0)) + } + + pub fn insert(&self, item: &dyn crate::MenuItemExt, position: usize) { + self.add_menu_item(item, AddOp::Insert(position)) + } + + fn add_menu_item(&self, item: &dyn crate::MenuItemExt, op: AddOp) { + let mut flags = 0; + let child = match item.type_() { + MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let child = &submenu.0 .0; + + flags |= MF_POPUP; + child + .borrow_mut() + .root_menu_haccel + .as_mut() + .unwrap() + .push(self.haccel.clone()); + + child + } + MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let child = &item.0 .0; + + flags |= MF_STRING; + + child + } + MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let child = &item.0 .0; + + let child_ = child.borrow(); + + match child_.predefined_item_type { + PredfinedMenuItemType::None => return, + PredfinedMenuItemType::Separator => { + flags |= MF_SEPARATOR; + } + _ => { + flags |= MF_STRING; + } + } + + child + } + MenuItemType::Check => { + let item = item.as_any().downcast_ref::().unwrap(); + let child = &item.0 .0; + + flags |= MF_STRING; + if child.borrow().checked { + flags |= MF_CHECKED; + } + + child + } } + .clone(); + + { + let child_ = child.borrow(); + if !child_.enabled { + flags |= MF_GRAYED; + } + + let mut text = child_.text.clone(); + + if let Some(accelerator) = &child_.accelerator { + let accel_str = accelerator.to_string(); + let accel = accelerator.to_accel(child_.id() as u16); + + text.push('\t'); + text.push_str(&accel_str); + + let mut haccel = self.haccel.borrow_mut(); + haccel.1.push(Accel(accel)); + let accels = haccel.1.clone(); + update_haccel(&mut haccel.0, accels) + } + + let id = child_.id() as usize; + + let text = encode_wide(text); + unsafe { + match op { + AddOp::Append => { + AppendMenuW(self.hmenu, flags, id, text.as_ptr()); + AppendMenuW(self.hpopupmenu, flags, id, text.as_ptr()); + } + AddOp::Insert(position) => { + InsertMenuW( + self.hmenu, + position as _, + flags | MF_BYPOSITION, + id, + text.as_ptr(), + ); + InsertMenuW( + self.hpopupmenu, + position as _, + flags | MF_BYPOSITION, + id, + text.as_ptr(), + ); + } + } + } + } + { + let mut child_ = child.borrow_mut(); + child_.parents_hemnu.push(self.hmenu); + child_.parents_hemnu.push(self.hpopupmenu); + } + { + let mut children = self.children.borrow_mut(); + match op { + AddOp::Append => children.push(child), + AddOp::Insert(position) => children.insert(position, child), + } + } + } + + pub fn remove(&self, item: &dyn crate::MenuItemExt) -> crate::Result<()> { unsafe { - AppendMenuW( - self.0.borrow().hmenu, - flags, - hmenu as _, - encode_wide(label.as_ref()).as_ptr(), - ) - }; - Submenu { - hmenu, - parent_hmenu: self.0.borrow().hmenu, - parent_menu: self.clone(), + RemoveMenu(self.hmenu, item.id(), MF_BYCOMMAND); + RemoveMenu(self.hpopupmenu, item.id(), MF_BYCOMMAND); + + for hwnd in self.hwnds.borrow().iter() { + DrawMenuBar(*hwnd); + } } + + let child = match item.type_() { + MenuItemType::Submenu => { + let item = item.as_any().downcast_ref::().unwrap(); + &item.0 .0 + } + MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + &item.0 .0 + } + MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + &item.0 .0 + } + MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + &item.0 .0 + } + }; + + { + let mut child = child.borrow_mut(); + let index = child + .parents_hemnu + .iter() + .position(|h| *h == self.hmenu) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + child.parents_hemnu.remove(index); + let index = child + .parents_hemnu + .iter() + .position(|h| *h == self.hpopupmenu) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + child.parents_hemnu.remove(index); + } + + let mut children = self.children.borrow_mut(); + let index = children + .iter() + .position(|e| e.borrow().id() == item.id()) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + children.remove(index); + + Ok(()) + } + + pub fn items(&self) -> Vec> { + self.children + .borrow() + .iter() + .map(|c| -> Box { + let child = c.borrow(); + match child.type_ { + MenuItemType::Submenu => Box::new(crate::Submenu(Submenu(c.clone()))), + MenuItemType::Normal => Box::new(crate::MenuItem(MenuItem(c.clone()))), + MenuItemType::Predefined => { + Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) + } + MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), + } + }) + .collect() + } + + fn find_by_id(&self, id: u32) -> Option>> { + let children = self.children.borrow(); + for i in children.iter() { + let item = i.borrow(); + if item.id == id { + return Some(i.clone()); + } + + if item.type_ == MenuItemType::Submenu { + let submenu = Submenu(i.clone()); + if let Some(child) = submenu.find_by_id(id) { + return Some(child); + } + } + } + None + } + + pub fn haccel(&self) -> HACCEL { + self.haccel.borrow().0 + } + + pub fn hpopupmenu(&self) -> HMENU { + self.hpopupmenu } pub fn init_for_hwnd(&self, hwnd: isize) { + self.hwnds.borrow_mut().push(hwnd); unsafe { - SetMenu(hwnd, self.0.borrow().hmenu); - SetWindowSubclass(hwnd, Some(menu_subclass_proc), MENU_SUBCLASS_ID, 0); + SetMenu(hwnd, self.hmenu); + SetWindowSubclass( + hwnd, + Some(menu_subclass_proc), + MENU_SUBCLASS_ID, + Box::into_raw(Box::new(self.clone())) as _, + ); DrawMenuBar(hwnd); }; } - pub fn haccel(&self) -> HACCEL { - self.0.borrow().haccel - } - - fn update_haccel(&mut self) { - let mut inner = self.0.borrow_mut(); - inner.haccel = unsafe { - CreateAcceleratorTableW(inner.accelerators.as_ptr(), inner.accelerators.len() as _) - }; - } - - pub fn remove_for_hwnd(&self, hwnd: isize) { + pub fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) { unsafe { + SetWindowSubclass( + hwnd, + Some(menu_subclass_proc), + MENU_SUBCLASS_ID, + Box::into_raw(Box::new(self.clone())) as _, + ); + } + } + + pub fn remove_for_hwnd(&self, hwnd: isize) -> crate::Result<()> { + let mut hwnds = self.hwnds.borrow_mut(); + let index = hwnds + .iter() + .position(|h| *h == hwnd) + .ok_or(crate::Error::NotInitialized)?; + hwnds.remove(index); + unsafe { + SendMessageW(hwnd, WM_CLEAR_MENU_DATA, 0, 0); RemoveWindowSubclass(hwnd, Some(menu_subclass_proc), MENU_SUBCLASS_ID); SetMenu(hwnd, 0); DrawMenuBar(hwnd); } + + Ok(()) } - pub fn hide_for_hwnd(&self, hwnd: isize) { + pub fn detach_menu_subclass_from_hwnd(&self, hwnd: isize) { + unsafe { + SendMessageW(hwnd, WM_CLEAR_MENU_DATA, 0, 0); + RemoveWindowSubclass(hwnd, Some(menu_subclass_proc), MENU_SUBCLASS_ID); + } + } + + pub fn hide_for_hwnd(&self, hwnd: isize) -> crate::Result<()> { + if !self.hwnds.borrow_mut().iter().any(|h| *h == hwnd) { + return Err(crate::Error::NotInitialized); + } + unsafe { SetMenu(hwnd, 0); DrawMenuBar(hwnd); } + + Ok(()) } - pub fn show_for_hwnd(&self, hwnd: isize) { + pub fn show_for_hwnd(&self, hwnd: isize) -> crate::Result<()> { + if !self.hwnds.borrow_mut().iter().any(|h| *h == hwnd) { + return Err(crate::Error::NotInitialized); + } + unsafe { - SetMenu(hwnd, self.0.borrow().hmenu); + SetMenu(hwnd, self.hmenu); DrawMenuBar(hwnd); } + + Ok(()) + } + + pub fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { + show_context_menu(hwnd, self.hpopupmenu, x, y) } } -const ABOUT_COUNTER_START: u64 = 400; -static ABOUT_COUNTER: Counter = Counter::new_with_start(ABOUT_COUNTER_START); -static mut ABOUT_MENU_ITEMS: Lazy> = Lazy::new(|| HashMap::new()); -static mut CHECK_MENU_ITEMS: Lazy> = Lazy::new(|| Vec::new()); - #[derive(Clone)] -pub struct Submenu { - hmenu: HMENU, - parent_hmenu: HMENU, - parent_menu: Menu, -} +pub(crate) struct Submenu(Rc>); impl Submenu { - pub fn label(&self) -> String { - let mut label = Vec::::new(); - - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STRING; - info.dwTypeData = label.as_mut_ptr(); - - unsafe { GetMenuItemInfoW(self.parent_hmenu, self.hmenu as _, false.into(), &mut info) }; - - info.cch += 1; - info.dwTypeData = Vec::with_capacity(info.cch as usize).as_mut_ptr(); - - unsafe { GetMenuItemInfoW(self.parent_hmenu, self.hmenu as _, false.into(), &mut info) }; - - // TOOD: check if it returns the label containing an ambersand and make gtk comply to that - decode_wide(info.dwTypeData) + pub fn new(text: &str, enabled: bool) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Submenu, + text: text.to_string(), + enabled, + parents_hemnu: Vec::new(), + children: Some(Vec::new()), + hmenu: unsafe { CreateMenu() }, + hpopupmenu: unsafe { CreatePopupMenu() }, + root_menu_haccel: Some(Vec::new()), + ..Default::default() + }))) } - pub fn set_label>(&mut self, label: S) { - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STRING; - info.dwTypeData = encode_wide(label.as_ref()).as_mut_ptr(); - - unsafe { SetMenuItemInfoW(self.parent_hmenu, self.hmenu as u32, false.into(), &info) }; + pub fn id(&self) -> u32 { + self.0.borrow().id() } - pub fn enabled(&self) -> bool { - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STATE; - - unsafe { GetMenuItemInfoW(self.parent_hmenu, self.hmenu as _, false.into(), &mut info) }; - - (info.fState & MFS_DISABLED) == 0 + pub fn hpopupmenu(&self) -> HMENU { + self.0.borrow().hpopupmenu } - pub fn set_enabled(&mut self, enabled: bool) { - unsafe { - EnableMenuItem( - self.parent_hmenu, - self.hmenu as _, - if enabled { MF_ENABLED } else { MF_DISABLED }, - ) - }; + pub fn append(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Append) } - pub fn add_submenu>(&mut self, label: S, enabled: bool) -> Submenu { - let hmenu = unsafe { CreateMenu() }; - let mut flags = MF_POPUP; - if !enabled { - flags |= MF_GRAYED; - } - unsafe { - AppendMenuW( - self.hmenu, - flags, - hmenu as _, - encode_wide(label.as_ref()).as_ptr(), - ) - }; - Submenu { - hmenu, - parent_hmenu: self.hmenu, - parent_menu: self.parent_menu.clone(), - } + pub fn prepend(&self, item: &dyn crate::MenuItemExt) { + self.add_menu_item(item, AddOp::Insert(0)) } - pub fn add_item>( - &mut self, - label: S, - enabled: bool, - accelerator: Option, - ) -> MenuItem { - let id = COUNTER.next(); - let mut flags = MF_STRING; - if !enabled { - flags |= MF_GRAYED; - } + pub fn insert(&self, item: &dyn crate::MenuItemExt, position: usize) { + self.add_menu_item(item, AddOp::Insert(position)) + } - let mut label = label.as_ref().to_string(); - if let Some(accelerator) = accelerator { - let accel_str = accelerator.to_string(); - let accel = accelerator.to_accel(id as u16); + fn add_menu_item(&self, item: &dyn crate::MenuItemExt, op: AddOp) { + let mut flags = 0; + let child = match item.type_() { + MenuItemType::Submenu => { + let submenu = item.as_any().downcast_ref::().unwrap(); + let child = &submenu.0 .0; - label.push_str("\t"); - label.push_str(&accel_str); - { - let mut parent_inner = self.parent_menu.0.borrow_mut(); - parent_inner.accelerators.push(accel); + flags |= MF_POPUP; + + child + .borrow_mut() + .root_menu_haccel + .as_mut() + .unwrap() + .extend_from_slice(self.0.borrow_mut().root_menu_haccel.as_ref().unwrap()); + + child } - self.parent_menu.update_haccel(); - } + MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + let child = &item.0 .0; - unsafe { AppendMenuW(self.hmenu, flags, id as _, encode_wide(label).as_ptr()) }; - MenuItem { - id, - parent_hmenu: self.hmenu, - } - } + flags |= MF_STRING; - 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), - NativeMenuItem::About(ref app_name, _) => { - let id = ABOUT_COUNTER.next(); - unsafe { - AppendMenuW( - self.hmenu, - MF_STRING, - id as _, - encode_wide(format!("About {}", app_name)).as_ptr(), - ); - ABOUT_MENU_ITEMS.insert(id, item); + child + } + + MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let child = &item.0 .0; + + let child_ = child.borrow(); + + match child_.predefined_item_type { + PredfinedMenuItemType::None => return, + PredfinedMenuItemType::Separator => { + flags |= MF_SEPARATOR; + } + _ => { + flags |= MF_STRING; + } } - return; + + child } - _ => return, - }; + MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + let child = &item.0 .0; + + flags |= MF_STRING; + if child.borrow().checked { + flags |= MF_CHECKED; + } + + child + } + } + .clone(); + + { + let mut self_ = self.0.borrow_mut(); + + let child_ = child.borrow(); + if !child_.enabled { + flags |= MF_GRAYED; + } + + let mut text = child_.text.clone(); + + if let Some(accelerator) = &child_.accelerator { + let accel_str = accelerator.to_string(); + let accel = accelerator.to_accel(child_.id() as u16); + + text.push('\t'); + text.push_str(&accel_str); + + for root_menu in self_.root_menu_haccel.as_mut().unwrap() { + let mut haccel = root_menu.borrow_mut(); + haccel.1.push(Accel(accel)); + let accels = haccel.1.clone(); + update_haccel(&mut haccel.0, accels) + } + } + + let id = child_.id() as usize; + let text = encode_wide(text); + unsafe { + match op { + AddOp::Append => { + AppendMenuW(self_.hmenu, flags, id, text.as_ptr()); + AppendMenuW(self_.hpopupmenu, flags, id, text.as_ptr()); + } + AddOp::Insert(position) => { + InsertMenuW( + self_.hmenu, + position as _, + flags | MF_BYPOSITION, + id, + text.as_ptr(), + ); + InsertMenuW( + self_.hpopupmenu, + position as _, + flags | MF_BYPOSITION, + id, + text.as_ptr(), + ); + } + } + } + } + { + let self_ = self.0.borrow(); + let mut child_ = child.borrow_mut(); + child_.parents_hemnu.push(self_.hmenu); + child_.parents_hemnu.push(self_.hpopupmenu); + } + { + let mut self_ = self.0.borrow_mut(); + let children = self_.children.as_mut().unwrap(); + match op { + AddOp::Append => children.push(child), + AddOp::Insert(position) => children.insert(position, child), + } + } + } + + pub fn remove(&self, item: &dyn crate::MenuItemExt) -> crate::Result<()> { unsafe { - AppendMenuW( - self.hmenu, - flags, - item.id() as _, - encode_wide(label).as_ptr(), - ) + RemoveMenu(self.0.borrow().hmenu, item.id(), MF_BYCOMMAND); + RemoveMenu(self.0.borrow().hpopupmenu, item.id(), MF_BYCOMMAND); + } + + let child = match item.type_() { + MenuItemType::Submenu => { + let item = item.as_any().downcast_ref::().unwrap(); + &item.0 .0 + } + MenuItemType::Normal => { + let item = item.as_any().downcast_ref::().unwrap(); + &item.0 .0 + } + MenuItemType::Predefined => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + &item.0 .0 + } + MenuItemType::Check => { + let item = item + .as_any() + .downcast_ref::() + .unwrap(); + &item.0 .0 + } }; + + { + let mut child = child.borrow_mut(); + let index = child + .parents_hemnu + .iter() + .position(|h| *h == self.0.borrow().hmenu) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + child.parents_hemnu.remove(index); + let index = child + .parents_hemnu + .iter() + .position(|h| *h == self.0.borrow().hpopupmenu) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + child.parents_hemnu.remove(index); + } + + let mut self_ = self.0.borrow_mut(); + let children = self_.children.as_mut().unwrap(); + let index = children + .iter() + .position(|e| e.borrow().id() == item.id()) + .ok_or(crate::Error::NotAChildOfThisMenu)?; + children.remove(index); + + Ok(()) } - pub fn add_check_item>( - &mut self, - label: S, - enabled: bool, - checked: bool, - accelerator: Option, - ) -> CheckMenuItem { - let mut item = CheckMenuItem(self.add_item(label, enabled, accelerator)); - item.set_checked(checked); - unsafe { CHECK_MENU_ITEMS.push(item.clone()) }; - item + pub fn items(&self) -> Vec> { + self.0 + .borrow() + .children + .as_ref() + .unwrap() + .iter() + .map(|c| -> Box { + let child = c.borrow(); + match child.type_ { + MenuItemType::Submenu => Box::new(crate::Submenu(Submenu(c.clone()))), + MenuItemType::Normal => Box::new(crate::MenuItem(MenuItem(c.clone()))), + MenuItemType::Predefined => { + Box::new(crate::PredefinedMenuItem(PredefinedMenuItem(c.clone()))) + } + MenuItemType::Check => Box::new(crate::CheckMenuItem(CheckMenuItem(c.clone()))), + } + }) + .collect() + } + + fn find_by_id(&self, id: u32) -> Option>> { + let self_ = self.0.borrow(); + let children = self_.children.as_ref().unwrap(); + for i in children.iter() { + let item = i.borrow(); + if item.id == id { + return Some(i.clone()); + } + + if item.type_ == MenuItemType::Submenu { + let submenu = Submenu(i.clone()); + if let Some(child) = submenu.find_by_id(id) { + return Some(child); + } + } + } + None + } + pub fn text(&self) -> String { + self.0.borrow().text() + } + + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) + } + + pub fn is_enabled(&self) -> bool { + self.0.borrow().is_enabled() + } + + pub fn set_enabled(&self, enabled: bool) { + self.0.borrow_mut().set_enabled(enabled) + } + + pub fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { + show_context_menu(hwnd, self.0.borrow().hpopupmenu, x, y) + } + + pub fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) { + unsafe { + SetWindowSubclass( + hwnd, + Some(menu_subclass_proc), + SUBMENU_SUBCLASS_ID, + Box::into_raw(Box::new(self.clone())) as _, + ); + } + } + + pub fn detach_menu_subclass_from_hwnd(&self, hwnd: isize) { + unsafe { + SendMessageW(hwnd, WM_CLEAR_MENU_DATA, 0, 0); + RemoveWindowSubclass(hwnd, Some(menu_subclass_proc), SUBMENU_SUBCLASS_ID); + } } } -#[derive(Clone)] -pub struct MenuItem { - id: u64, - parent_hmenu: HMENU, -} +#[derive(Clone, Debug)] +pub(crate) struct MenuItem(Rc>); impl MenuItem { - pub fn label(&self) -> String { - self.label_with_accel() - .split("\t") - .next() - .unwrap_or_default() - .to_string() + pub fn new(text: &str, enabled: bool, accelerator: Option) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Normal, + text: text.to_string(), + enabled, + parents_hemnu: Vec::new(), + id: COUNTER.next(), + accelerator, + ..Default::default() + }))) } - fn label_with_accel(&self) -> String { - let mut label = Vec::::new(); - - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STRING; - info.dwTypeData = label.as_mut_ptr(); - - unsafe { GetMenuItemInfoW(self.parent_hmenu, self.id as _, false.into(), &mut info) }; - - info.cch += 1; - info.dwTypeData = Vec::with_capacity(info.cch as usize).as_mut_ptr(); - - unsafe { GetMenuItemInfoW(self.parent_hmenu, self.id as _, false.into(), &mut info) }; - - decode_wide(info.dwTypeData) + pub fn id(&self) -> u32 { + self.0.borrow().id() } - pub fn set_label>(&mut self, label: S) { - let mut label = label.as_ref().to_string(); - let prev_label = self.label_with_accel(); - if let Some(accel_str) = prev_label.split("\t").nth(1) { - label.push_str("\t"); - label.push_str(accel_str); - } - - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STRING; - info.dwTypeData = encode_wide(label).as_mut_ptr(); - - unsafe { SetMenuItemInfoW(self.parent_hmenu, self.id as u32, false.into(), &info) }; + pub fn text(&self) -> String { + self.0.borrow().text() } - pub fn enabled(&self) -> bool { - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STATE; - - unsafe { GetMenuItemInfoW(self.parent_hmenu, self.id as _, false.into(), &mut info) }; - - (info.fState & MFS_DISABLED) == 0 + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) } - pub fn set_enabled(&mut self, enabled: bool) { - unsafe { - EnableMenuItem( - self.parent_hmenu, - self.id as _, - if enabled { MF_ENABLED } else { MF_DISABLED }, - ) - }; + pub fn is_enabled(&self) -> bool { + self.0.borrow().is_enabled() } - pub fn id(&self) -> u64 { - self.id + pub fn set_enabled(&self, enabled: bool) { + self.0.borrow_mut().set_enabled(enabled) } } -#[derive(Clone)] -pub struct CheckMenuItem(MenuItem); +#[derive(Clone, Debug)] +pub(crate) struct PredefinedMenuItem(Rc>); + +impl PredefinedMenuItem { + pub fn new(item_type: PredfinedMenuItemType, text: Option) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Predefined, + text: text.unwrap_or_else(|| item_type.text().to_string()), + enabled: true, + parents_hemnu: Vec::new(), + id: COUNTER.next(), + accelerator: item_type.accelerator(), + predefined_item_type: item_type, + ..Default::default() + }))) + } + + pub fn id(&self) -> u32 { + self.0.borrow().id() + } + + pub fn text(&self) -> String { + self.0.borrow().text() + } + + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct CheckMenuItem(Rc>); impl CheckMenuItem { - pub fn label(&self) -> String { - self.0.label() + pub fn new(text: &str, enabled: bool, checked: bool, accelerator: Option) -> Self { + Self(Rc::new(RefCell::new(MenuChild { + type_: MenuItemType::Check, + text: text.to_string(), + enabled, + parents_hemnu: Vec::new(), + id: COUNTER.next(), + accelerator, + checked, + ..Default::default() + }))) } - pub fn set_label>(&mut self, label: S) { - self.0.set_label(label) + pub fn id(&self) -> u32 { + self.0.borrow().id() } - pub fn enabled(&self) -> bool { - self.0.enabled() + pub fn text(&self) -> String { + self.0.borrow().text() } - pub fn set_enabled(&mut self, enabled: bool) { - self.0.set_enabled(enabled) + pub fn set_text(&self, text: &str) { + self.0.borrow_mut().set_text(text) } - pub fn checked(&self) -> bool { - let mut info: MENUITEMINFOW = unsafe { std::mem::zeroed() }; - info.cbSize = std::mem::size_of::() as _; - info.fMask = MIIM_STATE; - - unsafe { GetMenuItemInfoW(self.0.parent_hmenu, self.0.id as _, false.into(), &mut info) }; - - !((info.fState & MFS_CHECKED) == 0) + pub fn is_enabled(&self) -> bool { + self.0.borrow().is_enabled() } - pub fn set_checked(&mut self, checked: bool) { - use windows_sys::Win32::UI::WindowsAndMessaging; - unsafe { - WindowsAndMessaging::CheckMenuItem( - self.0.parent_hmenu, - self.0.id as _, - if checked { MF_CHECKED } else { MF_UNCHECKED }, - ) - }; + pub fn set_enabled(&self, enabled: bool) { + self.0.borrow_mut().set_enabled(enabled) } - pub fn id(&self) -> u64 { - self.0.id() + pub fn is_checked(&self) -> bool { + self.0.borrow().is_checked() + } + + pub fn set_checked(&self, checked: bool) { + self.0.borrow_mut().set_checked(checked) } } const MENU_SUBCLASS_ID: usize = 200; +const SUBMENU_SUBCLASS_ID: usize = 201; +const WM_CLEAR_MENU_DATA: u32 = 600; unsafe extern "system" fn menu_subclass_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, - _uidsubclass: usize, - _dwrefdata: usize, + uidsubclass: usize, + dwrefdata: usize, ) -> LRESULT { let mut ret = -1; + if msg == WM_DESTROY || msg == WM_CLEAR_MENU_DATA { + if uidsubclass == MENU_SUBCLASS_ID { + drop(Box::from_raw(dwrefdata as *mut Menu)); + } else { + drop(Box::from_raw(dwrefdata as *mut Submenu)); + } + } + if msg == WM_COMMAND { - let id = LOWORD(wparam as _) as u64; - - // Custom menu items - if COUNTER_START <= id && id <= COUNTER.current() { - // Toggle check menu items - // TODO: check the behavior in gtk - if let Some(item) = CHECK_MENU_ITEMS.iter_mut().find(|i| i.id() == id) { - item.set_checked(!item.checked()); - } - - let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); - ret = 0; + let id = util::LOWORD(wparam as _) as u32; + let item = if uidsubclass == MENU_SUBCLASS_ID { + let menu = dwrefdata as *mut Menu; + (*menu).find_by_id(id) + } else { + let menu = dwrefdata as *mut Submenu; + (*menu).find_by_id(id) }; - // Native menu items - if NativeMenuItem::is_id_of_native(id) { + if let Some(item) = item { 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); - } - _ if ABOUT_MENU_ITEMS.get(&id).is_some() => { - let item = ABOUT_MENU_ITEMS.get(&id).unwrap(); - if let NativeMenuItem::About(app_name, metadata) = item { - MessageBoxW( - hwnd, - encode_wide(format!( - r#" -{} + let mut dispatch = false; + + { + let mut item = item.borrow_mut(); + match item.type_ { + MenuItemType::Normal => { + dispatch = true; + } + MenuItemType::Check => { + dispatch = true; + + let checked = !item.checked; + item.set_checked(checked); + } + MenuItemType::Predefined => match &item.predefined_item_type { + PredfinedMenuItemType::Copy => execute_edit_command(EditCommand::Copy), + PredfinedMenuItemType::Cut => execute_edit_command(EditCommand::Cut), + PredfinedMenuItemType::Paste => execute_edit_command(EditCommand::Paste), + PredfinedMenuItemType::SelectAll => { + execute_edit_command(EditCommand::SelectAll) + } + PredfinedMenuItemType::Separator => {} + PredfinedMenuItemType::Minimize => { + ShowWindow(hwnd, SW_MINIMIZE); + } + PredfinedMenuItemType::CloseWindow => { + DestroyWindow(hwnd); + } + PredfinedMenuItemType::Quit => { + PostQuitMessage(0); + } + PredfinedMenuItemType::About(Some(metadata)) => { + MessageBoxW( + hwnd, + encode_wide(format!( + r#" + {} version: {} authors: {} license: {} website: {} {} + {} + {} + "#, + metadata.name.as_deref().unwrap_or_default(), + metadata.version.as_deref().unwrap_or_default(), + metadata.authors.as_deref().unwrap_or_default().join(","), + metadata.license.as_deref().unwrap_or_default(), + metadata.website_label.as_deref().unwrap_or_default(), + metadata.website.as_deref().unwrap_or_default(), + metadata.comments.as_deref().unwrap_or_default(), + metadata.copyright.as_deref().unwrap_or_default(), + )) + .as_ptr(), + encode_wide(format!( + "About {}", + metadata.name.as_deref().unwrap_or_default() + )) + .as_ptr(), + MB_ICONINFORMATION, + ); + } -{} - -{} - "#, - app_name, - metadata.version.as_deref().unwrap_or_default(), - metadata.authors.as_deref().unwrap_or_default().join(","), - metadata.license.as_deref().unwrap_or_default(), - metadata.website_label.as_deref().unwrap_or_default(), - metadata.website.as_deref().unwrap_or_default(), - metadata.comments.as_deref().unwrap_or_default(), - metadata.copyright.as_deref().unwrap_or_default(), - )) - .as_ptr(), - encode_wide(format!("About {}", &app_name)).as_ptr(), - MB_ICONINFORMATION, - ); - } + _ => {} + }, + _ => {} } - _ => unreachable!(), + } + + if dispatch { + let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); } } } @@ -487,6 +1038,35 @@ website: {} {} } } +fn update_haccel(haccel: &mut HMENU, accels: Vec) { + unsafe { + DestroyAcceleratorTable(*haccel); + *haccel = CreateAcceleratorTableW( + accels.iter().map(|i| i.0).collect::>().as_ptr(), + accels.len() as _, + ); + } +} + +fn show_context_menu(hwnd: HWND, hmenu: HMENU, x: f64, y: f64) { + unsafe { + let mut point = POINT { + x: x as _, + y: y as _, + }; + ClientToScreen(hwnd, &mut point); + TrackPopupMenu( + hmenu, + TPM_LEFTALIGN, + point.x, + point.y, + 0, + hwnd, + std::ptr::null(), + ); + } +} + enum EditCommand { Copy, Cut, @@ -523,23 +1103,3 @@ fn execute_edit_command(command: EditCommand) { SendInput(4, &inputs as *const _, std::mem::size_of::() as _); } } - -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) || (ABOUT_COUNTER_START <= id && id <= ABOUT_COUNTER.current()) - } -} diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 0eab037..1c81d55 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -1,19 +1,49 @@ -#[cfg(target_os = "windows")] +use std::ops::{Deref, DerefMut}; + +use windows_sys::Win32::UI::WindowsAndMessaging::ACCEL; + pub fn encode_wide>(string: S) -> Vec { std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) .chain(std::iter::once(0)) .collect() } -#[cfg(target_os = "windows")] #[allow(non_snake_case)] pub fn LOWORD(dword: u32) -> u16 { (dword & 0xFFFF) as u16 } -#[cfg(target_os = "windows")] pub fn decode_wide(w_str: *mut u16) -> String { let len = unsafe { windows_sys::Win32::Globalization::lstrlenW(w_str) } as usize; let w_str_slice = unsafe { std::slice::from_raw_parts(w_str, len) }; String::from_utf16_lossy(w_str_slice) } + +/// ACCEL wrapper to implement Debug +#[derive(Clone)] +#[repr(transparent)] +pub struct Accel(pub ACCEL); + +impl std::fmt::Debug for Accel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ACCEL") + .field("key", &self.0.key) + .field("cmd", &self.0.cmd) + .field("fVirt", &self.0.fVirt) + .finish() + } +} + +impl Deref for Accel { + type Target = ACCEL; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Accel { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/predefined.rs b/src/predefined.rs new file mode 100644 index 0000000..1f9287e --- /dev/null +++ b/src/predefined.rs @@ -0,0 +1,244 @@ +use crate::{accelerator::Accelerator, MenuItemExt, MenuItemType}; +use keyboard_types::{Code, Modifiers}; + +#[cfg(target_os = "macos")] +pub const CMD_OR_CTRL: Modifiers = Modifiers::META; +#[cfg(not(target_os = "macos"))] +pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL; + +/// A predefined (native) menu item which has a predfined behavior by the OS or by this crate. +pub struct PredefinedMenuItem(pub(crate) crate::platform_impl::PredefinedMenuItem); + +unsafe impl MenuItemExt for PredefinedMenuItem { + fn type_(&self) -> MenuItemType { + MenuItemType::Predefined + } + fn as_any(&self) -> &(dyn std::any::Any + 'static) { + self + } + + fn id(&self) -> u32 { + self.id() + } +} + +impl PredefinedMenuItem { + pub fn separator() -> PredefinedMenuItem { + PredefinedMenuItem::new::<&str>(PredfinedMenuItemType::Separator, None) + } + + pub fn copy(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Copy, text) + } + + pub fn cut(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Cut, text) + } + + pub fn paste(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Paste, text) + } + + pub fn select_all(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::SelectAll, text) + } + + pub fn undo(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Undo, text) + } + + pub fn redo(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Redo, text) + } + + pub fn minimize(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Minimize, text) + } + + pub fn maximize(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Maximize, text) + } + + pub fn fullscreen(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Fullscreen, text) + } + + pub fn hide(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Hide, text) + } + + pub fn hide_others(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::HideOthers, text) + } + + pub fn show_all(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::ShowAll, text) + } + + pub fn close_window(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::CloseWindow, text) + } + + pub fn quit(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Quit, text) + } + + pub fn about(text: Option<&str>, metadata: Option) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::About(metadata), text) + } + + pub fn services(text: Option<&str>) -> PredefinedMenuItem { + PredefinedMenuItem::new(PredfinedMenuItemType::Services, text) + } + fn new>(item: PredfinedMenuItemType, text: Option) -> Self { + Self(crate::platform_impl::PredefinedMenuItem::new( + item, + text.map(|t| t.as_ref().to_string()), + )) + } + + fn id(&self) -> u32 { + self.0.id() + } + + /// Get the text for this predefined menu item. + pub fn text(&self) -> String { + self.0.text() + } + + /// Set the text for this predefined menu item. + pub fn set_text>(&self, text: S) { + self.0.set_text(text.as_ref()) + } +} + +/// Application metadata for the [`PredefinedMenuItem::about`]. +/// +/// ## Platform-specific +/// +/// - **macOS:** The metadata is ignored. +#[derive(PartialEq, Eq, Debug, Clone, Default)] +pub struct AboutMetadata { + /// The application name. + pub name: Option, + /// The application version. + 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, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +#[non_exhaustive] +pub(crate) enum PredfinedMenuItemType { + Separator, + Copy, + Cut, + Paste, + SelectAll, + Undo, + Redo, + Minimize, + Maximize, + Fullscreen, + Hide, + HideOthers, + ShowAll, + CloseWindow, + Quit, + About(Option), + Services, + None, +} + +impl Default for PredfinedMenuItemType { + fn default() -> Self { + Self::None + } +} + +impl PredfinedMenuItemType { + pub(crate) fn text(&self) -> &str { + match self { + PredfinedMenuItemType::Separator => "", + PredfinedMenuItemType::Copy => "&Copy", + PredfinedMenuItemType::Cut => "Cu&t", + PredfinedMenuItemType::Paste => "&Paste", + PredfinedMenuItemType::SelectAll => "Select &All", + PredfinedMenuItemType::Undo => "Undo", + PredfinedMenuItemType::Redo => "Redo", + PredfinedMenuItemType::Minimize => "&Minimize", + #[cfg(target_os = "macos")] + PredfinedMenuItemType::Maximize => "Zoom", + #[cfg(not(target_os = "macos"))] + PredfinedMenuItemType::Maximize => "Maximize", + PredfinedMenuItemType::Fullscreen => "Toggle Full Screen", + PredfinedMenuItemType::Hide => "Hide", + PredfinedMenuItemType::HideOthers => "Hide Others", + PredfinedMenuItemType::ShowAll => "Show All", + #[cfg(windows)] + PredfinedMenuItemType::CloseWindow => "Close", + #[cfg(not(windows))] + PredfinedMenuItemType::CloseWindow => "C&lose Window", + #[cfg(windows)] + PredfinedMenuItemType::Quit => "&Exit", + #[cfg(not(windows))] + PredfinedMenuItemType::Quit => "&Quit", + PredfinedMenuItemType::About(_) => "&About", + PredfinedMenuItemType::Services => "Services", + PredfinedMenuItemType::None => "", + } + } + + pub(crate) fn accelerator(&self) -> Option { + match self { + PredfinedMenuItemType::Copy => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyC)), + PredfinedMenuItemType::Cut => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyX)), + PredfinedMenuItemType::Paste => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyV)), + PredfinedMenuItemType::Undo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyZ)), + #[cfg(target_os = "macos")] + PredfinedMenuItemType::Redo => Some(Accelerator::new( + Some(CMD_OR_CTRL | Modifiers::SHIFT), + Code::KeyZ, + )), + #[cfg(not(target_os = "macos"))] + PredfinedMenuItemType::Redo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyY)), + PredfinedMenuItemType::SelectAll => { + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyA)) + } + PredfinedMenuItemType::Minimize => { + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyM)) + } + #[cfg(target_os = "macos")] + PredfinedMenuItemType::Fullscreen => Some(Accelerator::new( + Some(Modifiers::META | Modifiers::CONTROL), + Code::KeyF, + )), + PredfinedMenuItemType::Hide => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)), + PredfinedMenuItemType::HideOthers => Some(Accelerator::new( + Some(CMD_OR_CTRL | Modifiers::ALT), + Code::KeyH, + )), + #[cfg(target_os = "macos")] + PredfinedMenuItemType::CloseWindow => { + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyW)) + } + #[cfg(not(target_os = "macos"))] + PredfinedMenuItemType::CloseWindow => { + Some(Accelerator::new(Some(Modifiers::ALT), Code::F4)) + } + #[cfg(target_os = "macos")] + PredfinedMenuItemType::Quit => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyQ)), + _ => None, + } + } +} diff --git a/src/submenu.rs b/src/submenu.rs new file mode 100644 index 0000000..9fa0696 --- /dev/null +++ b/src/submenu.rs @@ -0,0 +1,168 @@ +use crate::{ContextMenu, MenuItemExt, MenuItemType}; + +/// A menu that can be added to a [`Menu`] or another [`Submenu`]. +/// +/// [`Menu`]: crate::Menu +#[derive(Clone)] +pub struct Submenu(pub(crate) crate::platform_impl::Submenu); + +unsafe impl MenuItemExt for Submenu { + fn type_(&self) -> MenuItemType { + MenuItemType::Submenu + } + fn as_any(&self) -> &(dyn std::any::Any + 'static) { + self + } + + fn id(&self) -> u32 { + self.id() + } +} + +impl ContextMenu for Submenu { + #[cfg(target_os = "windows")] + fn hpopupmenu(&self) -> windows_sys::Win32::UI::WindowsAndMessaging::HMENU { + self.0.hpopupmenu() + } + + #[cfg(target_os = "windows")] + fn show_context_menu_for_hwnd(&self, hwnd: isize, x: f64, y: f64) { + self.0.show_context_menu_for_hwnd(hwnd, x, y) + } + + #[cfg(target_os = "windows")] + fn attach_menu_subclass_for_hwnd(&self, hwnd: isize) { + self.0.attach_menu_subclass_for_hwnd(hwnd) + } + + #[cfg(target_os = "windows")] + fn detach_menu_subclass_from_hwnd(&self, hwnd: isize) { + self.0.detach_menu_subclass_from_hwnd(hwnd) + } + + #[cfg(target_os = "linux")] + fn show_context_menu_for_gtk_window(&self, w: >k::ApplicationWindow, x: f64, y: f64) { + self.0.show_context_menu_for_gtk_window(w, x, y) + } + + #[cfg(target_os = "linux")] + fn gtk_context_menu(&self) -> gtk::Menu { + self.0.gtk_context_menu() + } + + #[cfg(target_os = "macos")] + fn show_context_menu_for_nsview(&self, view: cocoa::base::id, x: f64, y: f64) { + self.0.show_context_menu_for_nsview(view, x, y) + } +} + +impl Submenu { + /// Create a new submenu. + /// + /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic + /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&` + pub fn new>(text: S, enabled: bool) -> Self { + Self(crate::platform_impl::Submenu::new(text.as_ref(), enabled)) + } + + /// Creates a new submenu with given `items`. It calls [`Submenu::new`] and [`Submenu::append_items`] internally. + pub fn with_items>(text: S, enabled: bool, items: &[&dyn MenuItemExt]) -> Self { + let menu = Self::new(text, enabled); + menu.append_items(items); + menu + } + + /// Returns a unique identifier associated with this submenu. + pub fn id(&self) -> u32 { + self.0.id() + } + + /// Add a menu item to the end of this menu. + pub fn append(&self, item: &dyn MenuItemExt) { + self.0.append(item) + } + + /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop. + pub fn append_items(&self, items: &[&dyn MenuItemExt]) { + for item in items { + self.append(*item); + } + } + + /// Add a menu item to the beginning of this submenu. + pub fn prepend(&self, item: &dyn MenuItemExt) { + self.0.prepend(item) + } + + /// Add menu items to the beginning of this submenu. + /// It calls [`Menu::prepend`](crate::Menu::prepend) on the first element and + /// passes the rest to [`Menu::insert_items`](crate::Menu::insert_items) with position of `1`. + pub fn prepend_items(&self, items: &[&dyn MenuItemExt]) { + self.prepend(items[0]); + self.insert_items(&items[1..], 1); + } + + /// Insert a menu item at the specified `postion` in the submenu. + pub fn insert(&self, item: &dyn MenuItemExt, position: usize) { + self.0.insert(item, position) + } + + /// Insert menu items at the specified `postion` in the submenu. + pub fn insert_items(&self, items: &[&dyn MenuItemExt], position: usize) { + for (i, item) in items.iter().enumerate() { + self.insert(*item, position + i) + } + } + + /// Remove a menu item from this submenu. + pub fn remove(&self, item: &dyn MenuItemExt) -> crate::Result<()> { + self.0.remove(item) + } + + /// Returns a list of menu items that has been added to this submenu. + pub fn items(&self) -> Vec> { + self.0.items() + } + + /// Get the text for this submenu. + pub fn text(&self) -> String { + self.0.text() + } + + /// Set the text for this submenu. `text` could optionally contain + /// an `&` before a character to assign this character as the mnemonic + /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&` + pub fn set_text>(&self, text: S) { + self.0.set_text(text.as_ref()) + } + + /// Get whether this submenu is enabled or not. + pub fn is_enabled(&self) -> bool { + self.0.is_enabled() + } + + /// Enable or disable this submenu. + pub fn set_enabled(&self, enabled: bool) { + self.0.set_enabled(enabled) + } + + /// Set this submenu as the Window menu for the application on macOS. + /// + /// This will cause macOS to automatically add window-switching items and + /// certain other items to the menu. + #[cfg(target_os = "macos")] + pub fn set_windows_menu_for_nsapp(&self) { + self.0.set_windows_menu_for_nsapp() + } + + /// Set this submenu as the Help menu for the application on macOS. + /// + /// This will cause macOS to automatically add a search box to the menu. + /// + /// If no menu is set as the Help menu, macOS will automatically use any menu + /// which has a title matching the localized word "Help". + #[cfg(target_os = "macos")] + pub fn set_help_menu_for_nsapp(&self) { + self.0.set_help_menu_for_nsapp() + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..8bee045 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,25 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + +#[derive(Clone, Copy, Debug)] +pub enum AddOp { + Append, + Insert(usize), +} + +pub struct Counter(AtomicU32); + +impl Counter { + #[allow(unused)] + pub const fn new() -> Self { + Self(AtomicU32::new(1)) + } + + #[allow(unused)] + pub const fn new_with_start(start: u32) -> Self { + Self(AtomicU32::new(start)) + } + + pub fn next(&self) -> u32 { + self.0.fetch_add(1, Ordering::Relaxed) + } +}