feat: implement NativeMenuItem (#9)

* feat: implement `NativeMenuItem`

* windows: refactor native menu item handle in window proc

* native menu items on linux

* change about status to not implemented on windows
This commit is contained in:
Amr Bashir 2022-06-10 14:09:56 +02:00 committed by GitHub
parent 6f1c8cc9c9
commit 943beda6df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 568 additions and 187 deletions

View file

@ -21,11 +21,13 @@ features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_UI_Shell",
"Win32_Globalization"
"Win32_Globalization",
"Win32_UI_Input_KeyboardAndMouse"
]
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.15"
libxdo = "0.6.0"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.24"
@ -33,4 +35,4 @@ objc = "0.2"
[dev-dependencies]
winit = { git = "https://github.com/rust-windowing/winit" }
tao = { git = "https://github.com/tauri-apps/tao", branch = "muda/disable-gtk-menu-creation" }
tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }

View file

@ -1,4 +1,4 @@
use muda::{menu_event_receiver, Menu};
use muda::{menu_event_receiver, Menu, NativeMenuItem};
#[cfg(target_os = "linux")]
use tao::platform::unix::WindowExtUnix;
#[cfg(target_os = "windows")]
@ -16,16 +16,19 @@ fn main() {
let window2 = WindowBuilder::new().build(&event_loop).unwrap();
let mut menu_bar = Menu::new();
let mut file_menu = menu_bar.add_submenu("&File", true);
let mut edit_menu = menu_bar.add_submenu("&Edit", true);
let mut open_item = file_menu.add_text_item("&Open", true, None);
let mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S"));
let _quit_item = file_menu.add_text_item("&Quit", true, None);
file_menu.add_native_item(NativeMenuItem::Minimize);
file_menu.add_native_item(NativeMenuItem::CloseWindow);
file_menu.add_native_item(NativeMenuItem::Quit);
let _copy_item = edit_menu.add_text_item("&Copy", true, None);
let _cut_item = edit_menu.add_text_item("C&ut", true, None);
let mut edit_menu = menu_bar.add_submenu("&Edit", true);
edit_menu.add_native_item(NativeMenuItem::Cut);
edit_menu.add_native_item(NativeMenuItem::Copy);
edit_menu.add_native_item(NativeMenuItem::Paste);
edit_menu.add_native_item(NativeMenuItem::SelectAll);
#[cfg(target_os = "windows")]
{
@ -38,11 +41,6 @@ fn main() {
menu_bar.init_for_gtk_window(window2.gtk_window());
}
#[cfg(target_os = "macos")]
{
menu_bar.init_for_nsapp();
}
let menu_channel = menu_event_receiver();
let mut open_item_disabled = false;
let mut counter = 0;
@ -50,6 +48,21 @@ fn main() {
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
#[cfg(target_os = "macos")]
Event::NewEvents(tao::event::StartCause::Init) => {
menu_bar.init_for_nsapp();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
// window.request_redraw();
}
_ => (),
}
if let Ok(event) = menu_channel.try_recv() {
match event.id {
_ if event.id == save_item.id() => {
@ -66,16 +79,5 @@ fn main() {
_ => {}
}
}
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
window.request_redraw();
}
_ => (),
}
})
}

View file

@ -1,4 +1,4 @@
use muda::{menu_event_receiver, Menu};
use muda::{menu_event_receiver, Menu, NativeMenuItem};
#[cfg(target_os = "macos")]
use winit::platform::macOS::EventLoopExtMacOS;
#[cfg(target_os = "windows")]
@ -36,15 +36,17 @@ fn main() {
let _window2 = WindowBuilder::new().build(&event_loop).unwrap();
let mut file_menu = menu_bar.add_submenu("&File", true);
let mut edit_menu = menu_bar.add_submenu("&Edit", true);
let mut open_item = file_menu.add_text_item("&Open", true, Some("Ctrl+O"));
let mut open_item = file_menu.add_text_item("&Open", true, None);
let mut save_item = file_menu.add_text_item("&Save", true, Some("CommandOrCtrl+S"));
let _quit_item = file_menu.add_text_item("&Quit", true, None);
file_menu.add_native_item(NativeMenuItem::Minimize);
file_menu.add_native_item(NativeMenuItem::CloseWindow);
file_menu.add_native_item(NativeMenuItem::Quit);
let _copy_item = edit_menu.add_text_item("&Copy", true, Some("Ctrl+C"));
let _cut_item = edit_menu.add_text_item("C&ut", true, None);
let mut edit_menu = menu_bar.add_submenu("&Edit", true);
edit_menu.add_native_item(NativeMenuItem::Cut);
edit_menu.add_native_item(NativeMenuItem::Copy);
edit_menu.add_native_item(NativeMenuItem::Paste);
edit_menu.add_native_item(NativeMenuItem::SelectAll);
#[cfg(target_os = "windows")]
{
@ -64,6 +66,21 @@ fn main() {
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
#[cfg(target_os = "macos")]
Event::NewEvents(winit::event::StartCause::Init) => {
menu_bar.init_for_nsapp();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
window.request_redraw();
}
_ => (),
}
if let Ok(event) = menu_channel.try_recv() {
match event.id {
_ if event.id == save_item.id() => {
@ -80,20 +97,5 @@ fn main() {
_ => {}
}
}
match event {
#[cfg(target_os = "macos")]
Event::NewEvents(winit::event::StartCause::Init) => {
menu_bar.init_for_nsapp();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::MainEventsCleared => {
window.request_redraw();
}
_ => (),
}
})
}

View file

@ -114,7 +114,7 @@ impl Menu {
Submenu(self.0.add_submenu(label, enabled))
}
/// Adds this menu to a [`gtk::Window`]
/// Adds this menu to a [`gtk::ApplicationWindow`]
///
/// This method adds a [`gtk::Box`] then adds a [`gtk::MenuBar`] as its first child and returns the [`gtk::Box`].
/// So if more widgets need to be added, then [`gtk::prelude::BoxExt::pack_start`] or
@ -130,6 +130,7 @@ impl Menu {
#[cfg(target_os = "linux")]
pub fn init_for_gtk_window<W>(&self, w: &W) -> std::rc::Rc<gtk::Box>
where
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
W: gtk::prelude::IsA<gtk::Container>,
W: gtk::prelude::IsA<gtk::Window>,
{
@ -172,15 +173,11 @@ impl Menu {
self.0.haccel()
}
/// Removes this menu from a [`gtk::Window`]
///
/// ## Panics:
///
/// Panics if the window doesn't have a menu created by this crate.
/// Removes this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn remove_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::Container>,
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
W: gtk::prelude::IsA<gtk::Window>,
{
self.0.remove_for_gtk_window(w)
@ -192,12 +189,11 @@ impl Menu {
self.0.remove_for_hwnd(hwnd)
}
/// Hides this menu from a [`gtk::Window`]
/// Hides this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn hide_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::Container>,
W: gtk::prelude::IsA<gtk::Window>,
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.0.hide_for_gtk_window(w)
}
@ -208,12 +204,11 @@ impl Menu {
self.0.hide_for_hwnd(hwnd)
}
/// Shows this menu from a [`gtk::Window`]
/// Shows this menu from a [`gtk::ApplicationWindow`]
#[cfg(target_os = "linux")]
pub fn show_for_gtk_window<W>(&self, w: &W)
where
W: gtk::prelude::IsA<gtk::Container>,
W: gtk::prelude::IsA<gtk::Window>,
W: gtk::prelude::IsA<gtk::ApplicationWindow>,
{
self.0.show_for_gtk_window(w)
}
@ -290,6 +285,10 @@ impl Submenu {
) -> TextMenuItem {
TextMenuItem(self.0.add_text_item(label, enabled, accelerator))
}
pub fn add_native_item(&mut self, item: NativeMenuItem) {
self.0.add_native_item(item)
}
}
/// This is a Text menu item within a [`Submenu`].
@ -322,3 +321,126 @@ impl TextMenuItem {
self.0.id()
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum NativeMenuItem {
/// A native “About” menu item.
///
/// The first value is the application name, and the second is its metadata.
///
/// ## platform-specific:
///
/// - **macOS**: the metadata is ignore.
/// - **Windows**: Not implemented.
About(String, AboutMetadata),
/// A native “hide the app” menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux**: Unsupported.
Hide,
/// A native “hide all other windows" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux**: Unsupported.
HideOthers,
/// A native "Show all windows for this app" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux**: Unsupported.
ShowAll,
/// A native "Services" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux**: Unsupported.
Services,
/// A native "Close current window" menu item.
CloseWindow,
/// A native "Quit///
Quit,
/// A native "Copy" menu item.
///
/// ## Platform-specific:
///
/// - **macOS**: macOS require this menu item to enable "Copy" keyboard shortcut for your app.
/// - **Linux Wayland**: Not implmeneted.
Copy,
/// A native "Cut" menu item.
///
/// ## Platform-specific:
///
/// - **macOS**: macOS require this menu item to enable "Cut" keyboard shortcut for your app.
/// - **Linux Wayland**: Not implmeneted.
Cut,
/// A native "Paste" menu item.
///
/// ## Platform-specific:
///
/// - **macOS**: macOS require this menu item to enable "Paste" keyboard shortcut for your app.
/// - **Linux Wayland**: Not implmeneted.
Paste,
/// A native "Undo" menu item.
///
/// ## Platform-specific:
///
/// - **macOS**: macOS require this menu item to enable "Undo" keyboard shortcut for your app.
/// - **Windows / Linux**: Unsupported.
Undo,
/// A native "Redo" menu item.
///
/// ## Platform-specific:
///
/// - **macOS**: macOS require this menu item to enable "Redo" keyboard shortcut for your app.
/// - **Windows / Linux**: Unsupported.
Redo,
/// A native "Select All" menu item.
///
/// ## Platform-specific:
///
/// - **macOS**: macOS require this menu item to enable "Select All" keyboard shortcut for your app.
/// - **Linux Wayland**: Not implmeneted.
SelectAll,
/// A native "Enter fullscreen" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux**: Unsupported.
EnterFullScreen,
/// A native "Minimize current window" menu item.
Minimize,
/// A native "Zoom" menu item.
///
/// ## platform-specific:
///
/// - **Windows / Linux**: Unsupported.
Zoom,
/// Represends a Separator in the menu.
Separator,
}
/// Application metadata for the [`NativeMenuItem::About`].
///
/// ## Platform-specific
///
/// - **macOS**: The metadata is ignored.
#[derive(Debug, Clone, Default)]
pub struct AboutMetadata {
/// The application name.
pub version: Option<String>,
/// The authors of the application.
pub authors: Option<Vec<String>>,
/// Application comments.
pub comments: Option<String>,
/// The copyright of the application.
pub copyright: Option<String>,
/// The license of the application.
pub license: Option<String>,
/// The application website.
pub website: Option<String>,
/// The website label.
pub website_label: Option<String>,
}

View file

@ -1,41 +1,50 @@
mod accelerator;
use crate::counter::Counter;
use crate::{counter::Counter, NativeMenuItem};
use accelerator::{to_gtk_accelerator, to_gtk_menemenoic};
use gtk::{prelude::*, Orientation};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
static COUNTER: Counter = Counter::new();
#[derive(PartialEq, Eq)]
enum MenuEntryType {
Submenu,
Text,
}
/// Generic shared type describing a menu entry. It can be one of [`MenuEntryType`]
#[derive(Debug, Default)]
struct MenuEntry {
label: String,
enabled: bool,
r#type: MenuEntryType,
item_id: Option<u64>,
accelerator: Option<String>,
// NOTE(amrbashir): because gtk doesn't allow using the same `gtk::MenuItem`
native_menu_item: Option<NativeMenuItem>,
// NOTE(amrbashir): because gtk doesn't allow using the same [`gtk::MenuItem`]
// multiple times, and thus can't be used in multiple windows, each entry
// keeps a vector of a `gtk::MenuItem` or a tuple of `gtk::MenuItem` and `gtk::Menu`
// and push to it every time `Menu::init_for_gtk_window` is called.
item_gtk_items: Option<Rc<RefCell<Vec<gtk::MenuItem>>>>,
menu_gtk_items: Option<Rc<RefCell<Vec<(gtk::MenuItem, gtk::Menu)>>>>,
// keeps a vector of a [`gtk::MenuItem`] or a tuple of [`gtk::MenuItem`] and [`gtk::Menu`] if its a menu
// and push to it every time [`Menu::init_for_gtk_window`] is called.
native_items: Option<Rc<RefCell<Vec<gtk::MenuItem>>>>,
native_menus: Option<Rc<RefCell<Vec<(gtk::MenuItem, gtk::Menu)>>>>,
entries: Option<Vec<Rc<RefCell<MenuEntry>>>>,
}
#[derive(PartialEq, Eq, Debug)]
enum MenuEntryType {
Submenu,
Text,
Native,
}
impl Default for MenuEntryType {
fn default() -> Self {
MenuEntryType::Text
}
}
struct InnerMenu {
entries: Vec<Rc<RefCell<MenuEntry>>>,
// NOTE(amrbashir): because gtk doesn't allow using the same `gtk::MenuBar` and `gtk::Box`
// multiple times, and thus can't be used in multiple windows, entry
// keeps a vector of a tuple of `gtk::MenuBar` and `gtk::Box`
// NOTE(amrbashir): because gtk doesn't allow using the same [`gtk::MenuBar`] and [`gtk::Box`]
// multiple times, and thus can't be used in multiple windows. each menu
// keeps a hashmap of window pointer as the key and a tuple of [`gtk::MenuBar`] and [`gtk::Box`] as the value
// and push to it every time `Menu::init_for_gtk_window` is called.
gtk_items: HashMap<isize, (Option<gtk::MenuBar>, Rc<gtk::Box>)>,
native_menus: HashMap<isize, (Option<gtk::MenuBar>, Rc<gtk::Box>)>,
accel_group: gtk::AccelGroup,
}
@ -46,154 +55,117 @@ impl Menu {
pub fn new() -> Self {
Self(Rc::new(RefCell::new(InnerMenu {
entries: Vec::new(),
gtk_items: HashMap::new(),
native_menus: HashMap::new(),
accel_group: gtk::AccelGroup::new(),
})))
}
pub fn add_submenu(&mut self, label: impl AsRef<str>, enabled: bool) -> Submenu {
let label = label.as_ref().to_string();
let entry = Rc::new(RefCell::new(MenuEntry {
label: label.clone(),
label: label.as_ref().to_string(),
enabled,
entries: Some(Vec::new()),
r#type: MenuEntryType::Submenu,
item_id: None,
accelerator: None,
menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))),
item_gtk_items: None,
native_menus: Some(Rc::new(RefCell::new(Vec::new()))),
..Default::default()
}));
self.0.borrow_mut().entries.push(entry.clone());
Submenu(entry)
}
pub fn init_for_gtk_window<W>(&self, w: &W) -> Rc<gtk::Box>
pub fn init_for_gtk_window<W>(&self, window: &W) -> Rc<gtk::Box>
where
W: IsA<gtk::ApplicationWindow>,
W: IsA<gtk::Container>,
W: IsA<gtk::Window>,
{
let mut inner = self.0.borrow_mut();
// This is the first time this method has been called on a window
if inner.gtk_items.get(&(w.as_ptr() as _)).is_none() {
// This is the first time this method has been called on this window
// so we need to create the menubar and its parent box
if inner.native_menus.get(&(window.as_ptr() as _)).is_none() {
let menu_bar = gtk::MenuBar::new();
let vbox = gtk::Box::new(Orientation::Vertical, 0);
w.add(&vbox);
window.add(&vbox);
inner
.gtk_items
.insert(w.as_ptr() as _, (Some(menu_bar), Rc::new(vbox)));
.native_menus
.insert(window.as_ptr() as _, (Some(menu_bar), Rc::new(vbox)));
}
if let Some((menu_bar, vbox)) = inner.gtk_items.get(&(w.as_ptr() as _)) {
if let Some((menu_bar, vbox)) = inner.native_menus.get(&(window.as_ptr() as _)) {
// This is NOT the first time this method has been called on a window.
// So it already contains a `gtk::Box` but it doesn't have a `gtk::MenuBar`
// because it was probably removed using `Menu::remove_for_gtk_window`
// So it already contains a [`gtk::Box`] but it doesn't have a [`gtk::MenuBar`]
// because it was probably removed using [`Menu::remove_for_gtk_window`]
// so we only need to create the menubar
if menu_bar.is_none() {
let vbox = Rc::clone(vbox);
inner
.gtk_items
.insert(w.as_ptr() as _, (Some(gtk::MenuBar::new()), vbox));
.native_menus
.insert(window.as_ptr() as _, (Some(gtk::MenuBar::new()), vbox));
}
}
let (menu_bar, vbox) = inner.gtk_items.get(&(w.as_ptr() as _)).unwrap();
// Construct the entries of the menubar
let (menu_bar, vbox) = inner.native_menus.get(&(window.as_ptr() as _)).unwrap();
add_entries_to_menu(
menu_bar.as_ref().unwrap(),
&inner.entries,
&inner.accel_group,
);
w.add_accel_group(&inner.accel_group);
window.add_accel_group(&inner.accel_group);
// Show the menubar on the window
vbox.pack_start(menu_bar.as_ref().unwrap(), false, false, 0);
vbox.show_all();
Rc::clone(vbox)
}
pub fn remove_for_gtk_window<W>(&self, w: &W)
pub fn remove_for_gtk_window<W>(&self, window: &W)
where
W: IsA<gtk::Container>,
W: IsA<gtk::ApplicationWindow>,
W: IsA<gtk::Window>,
{
let mut inner = self.0.borrow_mut();
if let Some((menu_bar, vbox)) = inner.gtk_items.get(&(w.as_ptr() as _)) {
vbox.remove(menu_bar.as_ref().unwrap());
w.remove_accel_group(&inner.accel_group);
if let Some((Some(menu_bar), vbox)) = inner.native_menus.get(&(window.as_ptr() as _)) {
// Remove the [`gtk::Menubar`] from the widget tree
unsafe { menu_bar.destroy() };
// Detach the accelerators from the window
window.remove_accel_group(&inner.accel_group);
// Remove the removed [`gtk::Menubar`] from our cache
let vbox = Rc::clone(vbox);
inner.gtk_items.insert(w.as_ptr() as _, (None, vbox));
inner
.native_menus
.insert(window.as_ptr() as _, (None, vbox));
}
}
pub fn hide_for_gtk_window<W>(&self, w: &W)
pub fn hide_for_gtk_window<W>(&self, window: &W)
where
W: IsA<gtk::Container>,
W: IsA<gtk::Window>,
W: IsA<gtk::ApplicationWindow>,
{
if let Some((menu_bar, _)) = self.0.borrow().gtk_items.get(&(w.as_ptr() as isize)) {
if let Some(menu_bar) = menu_bar {
menu_bar.hide();
}
if let Some((Some(menu_bar), _)) = self
.0
.borrow()
.native_menus
.get(&(window.as_ptr() as isize))
{
menu_bar.hide();
}
}
pub fn show_for_gtk_window<W>(&self, w: &W)
pub fn show_for_gtk_window<W>(&self, window: &W)
where
W: IsA<gtk::Container>,
W: IsA<gtk::Window>,
W: IsA<gtk::ApplicationWindow>,
{
if let Some((menu_bar, _)) = self.0.borrow().gtk_items.get(&(w.as_ptr() as isize)) {
if let Some(menu_bar) = menu_bar {
menu_bar.show_all();
}
}
}
}
fn add_entries_to_menu<M: IsA<gtk::MenuShell>>(
gtk_menu: &M,
entries: &Vec<Rc<RefCell<MenuEntry>>>,
accel_group: &gtk::AccelGroup,
) {
for entry in entries {
let mut entry = entry.borrow_mut();
let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label));
gtk_menu.append(&gtk_item);
gtk_item.set_sensitive(entry.enabled);
if entry.r#type == MenuEntryType::Submenu {
let gtk_menu = gtk::Menu::new();
gtk_item.set_submenu(Some(&gtk_menu));
add_entries_to_menu(&gtk_menu, entry.entries.as_ref().unwrap(), accel_group);
entry
.menu_gtk_items
.as_mut()
.unwrap()
.borrow_mut()
.push((gtk_item, gtk_menu));
} else {
if let Some(accelerator) = &entry.accelerator {
let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator));
gtk_item.add_accelerator(
"activate",
accel_group,
key,
modifiers,
gtk::AccelFlags::VISIBLE,
);
}
let id = entry.item_id.unwrap_or_default();
gtk_item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
});
entry
.item_gtk_items
.as_mut()
.unwrap()
.borrow_mut()
.push(gtk_item);
if let Some((Some(menu_bar), _)) = self
.0
.borrow()
.native_menus
.get(&(window.as_ptr() as isize))
{
menu_bar.show_all();
}
}
}
@ -209,8 +181,8 @@ impl Submenu {
pub fn set_label(&mut self, label: impl AsRef<str>) {
let label = label.as_ref().to_string();
let mut entry = self.0.borrow_mut();
for (item, _) in entry.menu_gtk_items.as_ref().unwrap().borrow().iter() {
item.set_label(&label);
for (item, _) in entry.native_menus.as_ref().unwrap().borrow().iter() {
item.set_label(&to_gtk_menemenoic(&label));
}
entry.label = label;
}
@ -222,7 +194,7 @@ impl Submenu {
pub fn set_enabled(&mut self, enabled: bool) {
let mut entry = self.0.borrow_mut();
entry.enabled = true;
for (item, _) in entry.menu_gtk_items.as_ref().unwrap().borrow().iter() {
for (item, _) in entry.native_menus.as_ref().unwrap().borrow().iter() {
item.set_sensitive(enabled);
}
}
@ -233,10 +205,8 @@ impl Submenu {
enabled,
entries: Some(Vec::new()),
r#type: MenuEntryType::Submenu,
item_id: None,
accelerator: None,
menu_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))),
item_gtk_items: None,
native_menus: Some(Rc::new(RefCell::new(Vec::new()))),
..Default::default()
}));
self.0
.borrow_mut()
@ -254,14 +224,13 @@ impl Submenu {
accelerator: Option<&str>,
) -> TextMenuItem {
let entry = Rc::new(RefCell::new(MenuEntry {
label: to_gtk_menemenoic(label),
label: label.as_ref().to_string(),
enabled,
entries: None,
r#type: MenuEntryType::Text,
item_id: Some(COUNTER.next()),
accelerator: accelerator.map(|s| s.to_string()),
menu_gtk_items: None,
item_gtk_items: Some(Rc::new(RefCell::new(Vec::new()))),
native_items: Some(Rc::new(RefCell::new(Vec::new()))),
..Default::default()
}));
self.0
.borrow_mut()
@ -271,6 +240,15 @@ impl Submenu {
.push(entry.clone());
TextMenuItem(entry)
}
pub fn add_native_item(&mut self, item: NativeMenuItem) {
let entry = Rc::new(RefCell::new(MenuEntry {
r#type: MenuEntryType::Native,
native_menu_item: Some(item),
..Default::default()
}));
self.0.borrow_mut().entries.as_mut().unwrap().push(entry);
}
}
#[derive(Clone)]
@ -284,8 +262,8 @@ impl TextMenuItem {
pub fn set_label(&mut self, label: impl AsRef<str>) {
let label = label.as_ref().to_string();
let mut entry = self.0.borrow_mut();
for item in entry.item_gtk_items.as_ref().unwrap().borrow().iter() {
item.set_label(&label);
for item in entry.native_items.as_ref().unwrap().borrow().iter() {
item.set_label(&to_gtk_menemenoic(&label));
}
entry.label = label;
}
@ -296,7 +274,7 @@ impl TextMenuItem {
pub fn set_enabled(&mut self, enabled: bool) {
let mut entry = self.0.borrow_mut();
for item in entry.item_gtk_items.as_ref().unwrap().borrow().iter() {
for item in entry.native_items.as_ref().unwrap().borrow().iter() {
item.set_sensitive(enabled);
}
entry.enabled = enabled;
@ -306,3 +284,156 @@ impl TextMenuItem {
self.0.borrow().item_id.unwrap()
}
}
fn add_entries_to_menu<M>(
gtk_menu: &M,
entries: &Vec<Rc<RefCell<MenuEntry>>>,
accel_group: &gtk::AccelGroup,
) where
M: IsA<gtk::MenuShell>,
{
for entry in entries {
let mut entry = entry.borrow_mut();
match entry.r#type {
MenuEntryType::Submenu => {
let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label));
gtk_menu.append(&gtk_item);
gtk_item.set_sensitive(entry.enabled);
let gtk_menu = gtk::Menu::new();
gtk_item.set_submenu(Some(&gtk_menu));
add_entries_to_menu(&gtk_menu, entry.entries.as_ref().unwrap(), accel_group);
entry
.native_menus
.as_mut()
.unwrap()
.borrow_mut()
.push((gtk_item, gtk_menu));
}
MenuEntryType::Text => {
let gtk_item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(&entry.label));
gtk_menu.append(&gtk_item);
gtk_item.set_sensitive(entry.enabled);
if let Some(accelerator) = &entry.accelerator {
let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator));
gtk_item.add_accelerator(
"activate",
accel_group,
key,
modifiers,
gtk::AccelFlags::VISIBLE,
);
}
let id = entry.item_id.unwrap();
gtk_item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
});
entry
.native_items
.as_mut()
.unwrap()
.borrow_mut()
.push(gtk_item);
}
MenuEntryType::Native => match entry.native_menu_item.as_ref().unwrap() {
NativeMenuItem::Copy => {
let gtk_item = gtk::MenuItem::with_mnemonic("_Copy");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>X");
gtk_item
.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
gtk_item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+c", 0);
}
});
gtk_menu.append(&gtk_item);
}
NativeMenuItem::Cut => {
let gtk_item = gtk::MenuItem::with_mnemonic("Cu_t");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>X");
gtk_item
.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
gtk_item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+x", 0);
}
});
gtk_menu.append(&gtk_item);
}
NativeMenuItem::Paste => {
let gtk_item = gtk::MenuItem::with_mnemonic("_Paste");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>V");
gtk_item
.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
gtk_item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+v", 0);
}
});
gtk_menu.append(&gtk_item);
}
NativeMenuItem::SelectAll => {
let gtk_item = gtk::MenuItem::with_mnemonic("Select _All");
let (key, modifiers) = gtk::accelerator_parse("<Ctrl>A");
gtk_item
.child()
.unwrap()
.downcast::<gtk::AccelLabel>()
.unwrap()
.set_accel(key, modifiers);
gtk_item.connect_activate(move |_| {
// TODO: wayland
if let Ok(xdo) = libxdo::XDo::new(None) {
let _ = xdo.send_keysequence("ctrl+a", 0);
}
});
gtk_menu.append(&gtk_item);
}
NativeMenuItem::Separator => {
gtk_menu.append(&gtk::SeparatorMenuItem::new());
}
NativeMenuItem::Minimize => {
let gtk_item = gtk::MenuItem::with_mnemonic("_Minimize");
gtk_item.connect_activate(move |m| {
if let Some(window) = m.window() {
window.iconify()
}
});
gtk_menu.append(&gtk_item);
}
NativeMenuItem::CloseWindow => {
let gtk_item = gtk::MenuItem::with_mnemonic("C_lose Window");
gtk_item.connect_activate(move |m| {
if let Some(window) = m.window() {
window.destroy()
}
});
gtk_menu.append(&gtk_item);
}
NativeMenuItem::Quit => {
let gtk_item = gtk::MenuItem::with_mnemonic("_Quit");
gtk_item.connect_activate(move |_| {
std::process::exit(0);
});
gtk_menu.append(&gtk_item);
}
_ => {}
},
}
}
}

View file

@ -3,17 +3,19 @@
mod accelerator;
mod util;
use crate::counter::Counter;
use crate::{counter::Counter, NativeMenuItem};
use std::{cell::RefCell, rc::Rc};
use util::{decode_wide, encode_wide, LOWORD};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, WPARAM},
UI::{
Input::KeyboardAndMouse::{SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VK_CONTROL},
Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass},
WindowsAndMessaging::{
AppendMenuW, CreateAcceleratorTableW, CreateMenu, DrawMenuBar, EnableMenuItem,
GetMenuItemInfoW, SetMenu, SetMenuItemInfoW, ACCEL, HACCEL, HMENU, MENUITEMINFOW,
MFS_DISABLED, MF_DISABLED, MF_ENABLED, MF_GRAYED, MF_POPUP, MIIM_STATE, MIIM_STRING,
AppendMenuW, CloseWindow, CreateAcceleratorTableW, CreateMenu, DrawMenuBar,
EnableMenuItem, GetMenuItemInfoW, PostQuitMessage, SetMenu, SetMenuItemInfoW,
ShowWindow, ACCEL, HACCEL, HMENU, MENUITEMINFOW, MFS_DISABLED, MF_DISABLED, MF_ENABLED,
MF_GRAYED, MF_POPUP, MF_SEPARATOR, MF_STRING, MIIM_STATE, MIIM_STRING, SW_MINIMIZE,
WM_COMMAND,
},
},
@ -21,6 +23,7 @@ use windows_sys::Win32::{
use self::accelerator::parse_accelerator;
const COUNTER_START: u64 = 563;
static COUNTER: Counter = Counter::new_with_start(563);
const MENU_SUBCLASS_ID: usize = 232;
@ -189,7 +192,7 @@ impl Submenu {
accelerator: Option<&str>,
) -> TextMenuItem {
let id = COUNTER.next();
let mut flags = MF_POPUP;
let mut flags = MF_STRING;
if !enabled {
flags |= MF_GRAYED;
}
@ -218,6 +221,28 @@ impl Submenu {
parent_hmenu: self.hmenu,
}
}
pub fn add_native_item(&mut self, item: NativeMenuItem) {
let (label, flags) = match item {
NativeMenuItem::Copy => ("&Copy\tCtrl+C", MF_STRING),
NativeMenuItem::Cut => ("Cu&t\tCtrl+X", MF_STRING),
NativeMenuItem::Paste => ("&Paste\tCtrl+V", MF_STRING),
NativeMenuItem::SelectAll => ("Select&All", MF_STRING),
NativeMenuItem::Separator => ("", MF_SEPARATOR),
NativeMenuItem::Minimize => ("&Minimize", MF_STRING),
NativeMenuItem::CloseWindow => ("Close", MF_STRING),
NativeMenuItem::Quit => ("Exit", MF_STRING),
_ => return,
};
unsafe {
AppendMenuW(
self.hmenu,
flags,
item.id() as _,
encode_wide(label).as_ptr(),
)
};
}
}
#[derive(Clone)]
@ -302,10 +327,107 @@ unsafe extern "system" fn menu_subclass_proc(
_uidsubclass: usize,
_dwrefdata: usize,
) -> LRESULT {
let id = LOWORD(wparam as _);
if msg == WM_COMMAND && 0 < id && (id as u64) < COUNTER.current() {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id: id as _ });
let mut ret = -1;
if msg == WM_COMMAND {
let id = LOWORD(wparam as _) as u64;
// Custom menu items
if COUNTER_START < id && id < COUNTER.current() {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
ret = 0;
};
// Native menu items
if NativeMenuItem::is_id_of_native(id) {
ret = 0;
match id {
_ if id == NativeMenuItem::Copy.id() => {
execute_edit_command(EditCommand::Copy);
}
_ if id == NativeMenuItem::Cut.id() => {
execute_edit_command(EditCommand::Cut);
}
_ if id == NativeMenuItem::Paste.id() => {
execute_edit_command(EditCommand::Paste);
}
_ if id == NativeMenuItem::SelectAll.id() => {
execute_edit_command(EditCommand::SelectAll);
}
_ if id == NativeMenuItem::Minimize.id() => {
ShowWindow(hwnd, SW_MINIMIZE);
}
_ if id == NativeMenuItem::CloseWindow.id() => {
CloseWindow(hwnd);
}
_ if id == NativeMenuItem::Quit.id() => {
PostQuitMessage(0);
}
_ => unreachable!(),
}
}
}
if ret == -1 {
DefSubclassProc(hwnd, msg, wparam, lparam)
} else {
ret
}
}
enum EditCommand {
Copy,
Cut,
Paste,
SelectAll,
}
fn execute_edit_command(command: EditCommand) {
let key = match command {
EditCommand::Copy => 0x43, // c
EditCommand::Cut => 0x58, // x
EditCommand::Paste => 0x56, // v
EditCommand::SelectAll => 0x41, // a
};
DefSubclassProc(hwnd, msg, wparam, lparam)
unsafe {
let mut inputs: [INPUT; 4] = std::mem::zeroed();
inputs[0].r#type = INPUT_KEYBOARD;
inputs[0].Anonymous.ki.wVk = VK_CONTROL;
inputs[2].Anonymous.ki.dwFlags = 0;
inputs[1].r#type = INPUT_KEYBOARD;
inputs[1].Anonymous.ki.wVk = key;
inputs[2].Anonymous.ki.dwFlags = 0;
inputs[2].r#type = INPUT_KEYBOARD;
inputs[2].Anonymous.ki.wVk = key;
inputs[2].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP;
inputs[3].r#type = INPUT_KEYBOARD;
inputs[3].Anonymous.ki.wVk = VK_CONTROL;
inputs[3].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP;
let ret = SendInput(4, &inputs as *const _, std::mem::size_of::<INPUT>() as _);
dbg!(ret);
}
}
impl NativeMenuItem {
fn id(&self) -> u64 {
match self {
NativeMenuItem::Copy => 301,
NativeMenuItem::Cut => 302,
NativeMenuItem::Paste => 303,
NativeMenuItem::SelectAll => 304,
NativeMenuItem::Separator => 305,
NativeMenuItem::Minimize => 306,
NativeMenuItem::CloseWindow => 307,
NativeMenuItem::Quit => 308,
_ => unreachable!(),
}
}
fn is_id_of_native(id: u64) -> bool {
(301..=308).contains(&id)
}
}