216 lines
8.1 KiB
Rust
216 lines
8.1 KiB
Rust
//! Safe macOS menu adapter over the shared desktop action registry.
|
|
|
|
use muda::{
|
|
Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
|
|
accelerator::{Accelerator, Code, Modifiers},
|
|
};
|
|
|
|
use crate::action::{self, ActionContext, MenuGroup, UiAction};
|
|
|
|
pub struct NativeMenu {
|
|
menu: Menu,
|
|
window_menu: Submenu,
|
|
help_menu: Submenu,
|
|
items: Vec<(UiAction, MenuItem)>,
|
|
}
|
|
|
|
impl NativeMenu {
|
|
pub fn install(context: ActionContext) -> Result<Self, muda::Error> {
|
|
let menu = Menu::new();
|
|
let mut items = Vec::new();
|
|
let mut window_menu = None;
|
|
let mut help_menu = None;
|
|
for group in MenuGroup::ALL {
|
|
let submenu = Submenu::new(group.label(), true);
|
|
match group {
|
|
MenuGroup::App => {
|
|
append_action(&submenu, UiAction::About, context, &mut items)?;
|
|
submenu.append(&PredefinedMenuItem::separator())?;
|
|
append_action(&submenu, UiAction::Settings, context, &mut items)?;
|
|
submenu.append(&PredefinedMenuItem::separator())?;
|
|
submenu.append(&PredefinedMenuItem::services(None))?;
|
|
submenu.append(&PredefinedMenuItem::separator())?;
|
|
submenu.append(&PredefinedMenuItem::hide(None))?;
|
|
submenu.append(&PredefinedMenuItem::hide_others(None))?;
|
|
submenu.append(&PredefinedMenuItem::show_all(None))?;
|
|
submenu.append(&PredefinedMenuItem::separator())?;
|
|
append_action(&submenu, UiAction::Quit, context, &mut items)?;
|
|
}
|
|
MenuGroup::Window => {
|
|
submenu.append(&PredefinedMenuItem::minimize(None))?;
|
|
submenu.append(&PredefinedMenuItem::bring_all_to_front(None))?;
|
|
}
|
|
MenuGroup::Edit => {
|
|
submenu.append(&PredefinedMenuItem::undo(None))?;
|
|
submenu.append(&PredefinedMenuItem::redo(None))?;
|
|
submenu.append(&PredefinedMenuItem::separator())?;
|
|
submenu.append(&PredefinedMenuItem::cut(None))?;
|
|
submenu.append(&PredefinedMenuItem::copy(None))?;
|
|
submenu.append(&PredefinedMenuItem::paste(None))?;
|
|
submenu.append(&PredefinedMenuItem::select_all(None))?;
|
|
submenu.append(&PredefinedMenuItem::separator())?;
|
|
append_action(&submenu, UiAction::Find, context, &mut items)?;
|
|
append_action(&submenu, UiAction::SearchContents, context, &mut items)?;
|
|
}
|
|
_ => append_actions(&submenu, group, context, &mut items)?,
|
|
}
|
|
if group == MenuGroup::Window {
|
|
window_menu = Some(submenu.clone());
|
|
} else if group == MenuGroup::Help {
|
|
help_menu = Some(submenu.clone());
|
|
}
|
|
menu.append(&submenu)?;
|
|
}
|
|
let native = Self {
|
|
menu,
|
|
window_menu: window_menu.expect("the Window menu is registered"),
|
|
help_menu: help_menu.expect("the Help menu is registered"),
|
|
items,
|
|
};
|
|
native.activate();
|
|
Ok(native)
|
|
}
|
|
|
|
pub fn activate(&self) {
|
|
self.menu.init_for_nsapp();
|
|
self.window_menu.set_as_windows_menu_for_nsapp();
|
|
self.help_menu.set_as_help_menu_for_nsapp();
|
|
}
|
|
|
|
pub fn sync(&self, context: ActionContext) {
|
|
for (action, item) in &self.items {
|
|
item.set_enabled(action::enabled(*action, context));
|
|
}
|
|
}
|
|
|
|
pub fn poll(&self) -> Option<UiAction> {
|
|
let event = MenuEvent::receiver().try_recv().ok()?;
|
|
self.items
|
|
.iter()
|
|
.find_map(|(action, item)| (item.id() == &event.id).then_some(*action))
|
|
}
|
|
}
|
|
|
|
fn append_actions(
|
|
submenu: &Submenu,
|
|
group: MenuGroup,
|
|
context: ActionContext,
|
|
items: &mut Vec<(UiAction, MenuItem)>,
|
|
) -> Result<(), muda::Error> {
|
|
for spec in action::actions_in(group).filter(|spec| {
|
|
!matches!(
|
|
spec.action,
|
|
UiAction::About
|
|
| UiAction::Undo
|
|
| UiAction::Redo
|
|
| UiAction::Cut
|
|
| UiAction::CopyEditedField
|
|
| UiAction::Paste
|
|
| UiAction::Minimize
|
|
| UiAction::TogglePaneFocus
|
|
)
|
|
}) {
|
|
append_action(submenu, spec.action, context, items)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn append_action(
|
|
submenu: &Submenu,
|
|
action: UiAction,
|
|
context: ActionContext,
|
|
items: &mut Vec<(UiAction, MenuItem)>,
|
|
) -> Result<(), muda::Error> {
|
|
let spec = action::spec_for(action);
|
|
let item = MenuItem::with_id(
|
|
format!("ironstorage.{}", action.id()),
|
|
spec.label,
|
|
action::enabled(action, context),
|
|
accelerator(action),
|
|
);
|
|
submenu.append(&item)?;
|
|
items.push((action, item));
|
|
Ok(())
|
|
}
|
|
|
|
fn accelerator(action: UiAction) -> Option<Accelerator> {
|
|
let command = Modifiers::SUPER;
|
|
let (modifiers, code) = match action {
|
|
UiAction::Settings => (command, Code::Comma),
|
|
UiAction::NewEntry => (command, Code::KeyN),
|
|
UiAction::NewFolder => (command | Modifiers::SHIFT, Code::KeyN),
|
|
UiAction::OpenFolder => (command, Code::KeyO),
|
|
UiAction::Save => (command, Code::KeyS),
|
|
UiAction::CloseWindow => (command, Code::KeyW),
|
|
UiAction::Quit => (command, Code::KeyQ),
|
|
UiAction::Undo => (command, Code::KeyZ),
|
|
UiAction::Redo => (command | Modifiers::SHIFT, Code::KeyZ),
|
|
UiAction::Cut => (command, Code::KeyX),
|
|
UiAction::Paste => (command, Code::KeyV),
|
|
UiAction::Find => (command, Code::KeyF),
|
|
UiAction::SearchContents => (command | Modifiers::SHIFT, Code::KeyF),
|
|
UiAction::CommandPalette => (command, Code::KeyK),
|
|
UiAction::Refresh => (command, Code::KeyR),
|
|
UiAction::ReloadEntry => (command | Modifiers::SHIFT, Code::KeyR),
|
|
UiAction::EditEntry => (command, Code::KeyE),
|
|
UiAction::Lock => (command, Code::KeyL),
|
|
UiAction::Help => (Modifiers::empty(), Code::F1),
|
|
UiAction::About
|
|
| UiAction::InitializeStore
|
|
| UiAction::CopyField
|
|
| UiAction::CopyEditedField
|
|
| UiAction::TogglePaneFocus
|
|
| UiAction::OpenEntry
|
|
| UiAction::GeneratePassword
|
|
| UiAction::MoveEntry
|
|
| UiAction::CopyEntry
|
|
| UiAction::DeleteEntry
|
|
| UiAction::GenerateOtp
|
|
| UiAction::CopyOtp
|
|
| UiAction::ImportOtp
|
|
| UiAction::ImportKdbx
|
|
| UiAction::ShowOtpUri
|
|
| UiAction::CopyOtpUri
|
|
| UiAction::ShowOtpQr
|
|
| UiAction::RemoveOtp
|
|
| UiAction::GitStatus
|
|
| UiAction::GitPull
|
|
| UiAction::GitPush
|
|
| UiAction::GitSync
|
|
| UiAction::Minimize => return None,
|
|
};
|
|
Some(Accelerator::new(Some(modifiers), code))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn native_menu_uses_standard_macos_accelerators() {
|
|
for (action, modifiers, code) in [
|
|
(UiAction::Settings, Modifiers::SUPER, Code::Comma),
|
|
(UiAction::NewEntry, Modifiers::SUPER, Code::KeyN),
|
|
(UiAction::OpenFolder, Modifiers::SUPER, Code::KeyO),
|
|
(UiAction::Save, Modifiers::SUPER, Code::KeyS),
|
|
(UiAction::CloseWindow, Modifiers::SUPER, Code::KeyW),
|
|
(UiAction::Quit, Modifiers::SUPER, Code::KeyQ),
|
|
(UiAction::Find, Modifiers::SUPER, Code::KeyF),
|
|
(
|
|
UiAction::SearchContents,
|
|
Modifiers::SUPER | Modifiers::SHIFT,
|
|
Code::KeyF,
|
|
),
|
|
(UiAction::CommandPalette, Modifiers::SUPER, Code::KeyK),
|
|
(UiAction::Refresh, Modifiers::SUPER, Code::KeyR),
|
|
(UiAction::Lock, Modifiers::SUPER, Code::KeyL),
|
|
(UiAction::Help, Modifiers::empty(), Code::F1),
|
|
] {
|
|
let accelerator = accelerator(action).expect("registered native accelerator");
|
|
assert_eq!(accelerator.modifiers(), modifiers, "{action:?}");
|
|
assert_eq!(accelerator.key(), code, "{action:?}");
|
|
}
|
|
assert!(accelerator(UiAction::GeneratePassword).is_none());
|
|
}
|
|
}
|