feat: add accelerator module (#17)

* Add accelerator module

* Add Linux port

* Add macOS port

* Add Windows port

* Remove unused types

* Fix doc tests

* Add more variants
This commit is contained in:
Ngo Iok Ui (Wu Yu Wei) 2022-07-20 20:34:09 +08:00 committed by GitHub
parent 28ffd206fa
commit e33c5f0daf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 931 additions and 326 deletions

View file

@ -13,6 +13,7 @@ categories = ["gui"]
[dependencies] [dependencies]
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
once_cell = "1.10" once_cell = "1.10"
keyboard-types = "0.6"
[target.'cfg(target_os = "windows")'.dependencies.windows-sys] [target.'cfg(target_os = "windows")'.dependencies.windows-sys]
version = "0.34" version = "0.34"
@ -26,6 +27,7 @@ features = [
] ]
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gdk = "0.15"
gtk = "0.15" gtk = "0.15"
libxdo = "0.6.0" libxdo = "0.6.0"

View file

@ -1,4 +1,8 @@
use muda::{menu_event_receiver, Menu, NativeMenuItem}; use keyboard_types::Code;
use muda::{
accelerator::{Accelerator, Mods},
menu_event_receiver, Menu, NativeMenuItem,
};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use tao::platform::unix::WindowExtUnix; use tao::platform::unix::WindowExtUnix;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -19,7 +23,11 @@ fn main() {
let mut file_menu = menu_bar.add_submenu("&File", true); let mut file_menu = menu_bar.add_submenu("&File", true);
let mut open_item = file_menu.add_item("&Open", true, None); let mut open_item = file_menu.add_item("&Open", true, None);
let mut save_item = file_menu.add_item("&Save", true, Some("CommandOrCtrl+S")); let mut save_item = file_menu.add_item(
"&Save",
true,
Some(Accelerator::new(Mods::Ctrl, Code::KeyS)),
);
file_menu.add_native_item(NativeMenuItem::Minimize); file_menu.add_native_item(NativeMenuItem::Minimize);
file_menu.add_native_item(NativeMenuItem::CloseWindow); file_menu.add_native_item(NativeMenuItem::CloseWindow);
file_menu.add_native_item(NativeMenuItem::Quit); file_menu.add_native_item(NativeMenuItem::Quit);

View file

@ -1,4 +1,8 @@
use muda::{menu_event_receiver, Menu, NativeMenuItem}; use keyboard_types::Code;
use muda::{
accelerator::{Accelerator, Mods},
menu_event_receiver, Menu, NativeMenuItem,
};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use winit::platform::macos::EventLoopBuilderExtMacOS; use winit::platform::macos::EventLoopBuilderExtMacOS;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -37,7 +41,11 @@ fn main() {
let mut file_menu = menu_bar.add_submenu("&File", true); let mut file_menu = menu_bar.add_submenu("&File", true);
let mut open_item = file_menu.add_item("&Open", true, None); let mut open_item = file_menu.add_item("&Open", true, None);
let mut save_item = file_menu.add_item("&Save", true, Some("CommandOrCtrl+S")); let mut save_item = file_menu.add_item(
"&Save",
true,
Some(Accelerator::new(Mods::Ctrl, Code::KeyS)),
);
file_menu.add_native_item(NativeMenuItem::Minimize); file_menu.add_native_item(NativeMenuItem::Minimize);
file_menu.add_native_item(NativeMenuItem::CloseWindow); file_menu.add_native_item(NativeMenuItem::CloseWindow);
file_menu.add_native_item(NativeMenuItem::Quit); file_menu.add_native_item(NativeMenuItem::Quit);

288
src/accelerator.rs Normal file
View file

@ -0,0 +1,288 @@
//! Accelerators describe keyboard shortcuts defined by the application.
//!
//! [`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
//! 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);
//! 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};
//! #
//! 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::<Accelerator>().is_err());
//! ```
//!
pub use keyboard_types::{Code, Modifiers};
use std::{borrow::Borrow, hash::Hash, str::FromStr};
/// Base `Accelerator` functions.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct Accelerator {
pub(crate) mods: Modifiers,
pub(crate) key: Code,
}
impl Accelerator {
/// Creates a new accelerator to define keyboard shortcuts throughout your application.
pub fn new(mods: impl Into<Option<Modifiers>>, key: Code) -> Self {
Self {
mods: mods.into().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<Modifiers>, key: impl Borrow<Code>) -> 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 modifiers = modifiers.borrow();
let key = key.borrow();
self.mods == *modifiers & base_mods && self.key == *key
}
}
// Accelerator::from_str is available to be backward
// compatible with tauri and it also open the option
// to generate accelerator from string
impl FromStr for Accelerator {
type Err = AcceleratorParseError;
fn from_str(accelerator_string: &str) -> Result<Self, Self::Err> {
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<Mods> for Option<Modifiers> {
fn from(src: Mods) -> Option<Modifiers> {
Some(src.into())
}
}
impl From<Mods> 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<Accelerator, AcceleratorParseError> {
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(
"Unexpected empty token while parsing accelerator".into(),
));
}
if key != Code::Unidentified {
// at this point we already parsed the modifiers and found a main key but
// the function received more then one main key or it is not in the right order
// examples:
// 1. "Ctrl+Shift+C+A" => only one main key should be allowd.
// 2. "Ctrl+C+Shift" => wrong order
return Err(AcceleratorParseError(format!(
"Unexpected accelerator string format: \"{}\"",
accelerator_string
)));
}
match token.to_uppercase().as_str() {
"OPTION" | "ALT" => {
mods.set(Modifiers::ALT, true);
}
"CONTROL" | "CTRL" => {
mods.set(Modifiers::CONTROL, true);
}
"COMMAND" | "CMD" | "SUPER" => {
mods.set(Modifiers::SUPER, true);
}
"SHIFT" => {
mods.set(Modifiers::SHIFT, true);
}
"COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
#[cfg(target_os = "macos")]
mods.set(Modifiers::SUPER, true);
#[cfg(not(target_os = "macos"))]
mods.set(Modifiers::CONTROL, true);
}
_ => {
if let Ok(code) = Code::from_str(token.as_str()) {
match code {
Code::Unidentified => {
return Err(AcceleratorParseError(format!(
"Couldn't identify \"{}\" as a valid `Code`",
token
)))
}
_ => key = code,
}
} else {
return Err(AcceleratorParseError(format!(
"Couldn't identify \"{}\" as a valid `Code`",
token
)));
}
}
}
}
Ok(Accelerator { key, mods })
}
#[test]
fn test_parse_accelerator() {
assert_eq!(
parse_accelerator("CTRL+KeyX").unwrap(),
Accelerator {
mods: Modifiers::CONTROL,
key: Code::KeyX,
}
);
assert_eq!(
parse_accelerator("SHIFT+KeyC").unwrap(),
Accelerator {
mods: Modifiers::SHIFT,
key: Code::KeyC,
}
);
assert_eq!(
parse_accelerator("CTRL+KeyZ").unwrap(),
Accelerator {
mods: Modifiers::CONTROL,
key: Code::KeyZ,
}
);
assert_eq!(
parse_accelerator("super+ctrl+SHIFT+alt+ArrowUp").unwrap(),
Accelerator {
mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
key: Code::ArrowUp,
}
);
assert_eq!(
parse_accelerator("Digit5").unwrap(),
Accelerator {
mods: Modifiers::empty(),
key: Code::Digit5,
}
);
assert_eq!(
parse_accelerator("KeyG").unwrap(),
Accelerator {
mods: Modifiers::empty(),
key: Code::KeyG,
}
);
let acc = parse_accelerator("+G");
assert!(acc.is_err());
let acc = parse_accelerator("SHGSH+G");
assert!(acc.is_err());
assert_eq!(
parse_accelerator("SHiFT+F12").unwrap(),
Accelerator {
mods: Modifiers::SHIFT,
key: Code::F12,
}
);
assert_eq!(
parse_accelerator("CmdOrCtrl+Space").unwrap(),
Accelerator {
#[cfg(target_os = "macos")]
mods: Modifiers::SUPER,
#[cfg(not(target_os = "macos"))]
mods: Modifiers::CONTROL,
key: Code::Space,
}
);
let acc = parse_accelerator("CTRL+");
assert!(acc.is_err());
}

View file

@ -3,14 +3,14 @@
//! //!
//! Before you can add submenus and menu items, you first need a root or a base menu. //! Before you can add submenus and menu items, you first need a root or a base menu.
//! ```no_run //! ```no_run
//! let mut menu = Menu::new(); //! let mut menu = muda::Menu::new();
//! ``` //! ```
//! //!
//! # Adding submens to the root menu //! # Adding submens to the root menu
//! //!
//! Once you have a root menu you can start adding [`Submenu`]s by using [`Menu::add_submenu`]. //! Once you have a root menu you can start adding [`Submenu`]s by using [`Menu::add_submenu`].
//! ```no_run //! ```no_run
//! let mut menu = Menu::new(); //! let mut menu = muda::Menu::new();
//! let file_menu = menu.add_submenu("File", true); //! let file_menu = menu.add_submenu("File", true);
//! let edit_menu = menu.add_submenu("Edit", true); //! let edit_menu = menu.add_submenu("Edit", true);
//! ``` //! ```
@ -19,22 +19,22 @@
//! //!
//! Once you have a [`Submenu`] you can star creating more [`Submenu`]s or [`MenuItem`]s. //! Once you have a [`Submenu`] you can star creating more [`Submenu`]s or [`MenuItem`]s.
//! ```no_run //! ```no_run
//! let mut menu = Menu::new(); //! let mut menu = muda::Menu::new();
//! //!
//! let file_menu = menu.add_submenu("File", true); //! let mut file_menu = menu.add_submenu("File", true);
//! let open_item = file_menu.add_text_item("Open", true); //! let open_item = file_menu.add_item("Open", true, None);
//! let save_item = file_menu.add_text_item("Save", true); //! let save_item = file_menu.add_item("Save", true, None);
//! //!
//! let edit_menu = menu.add_submenu("Edit", true); //! let mut edit_menu = menu.add_submenu("Edit", true);
//! let copy_item = file_menu.add_text_item("Copy", true); //! let copy_item = file_menu.add_item("Copy", true, None);
//! let cut_item = file_menu.add_text_item("Cut", true); //! let cut_item = file_menu.add_item("Cut", true, None);
//! ``` //! ```
//! //!
//! # Add your root menu to a Window (Windows and Linux Only) //! # 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. //! You can use [`Menu`] to display a top menu in a Window on Windows and Linux.
//! ```no_run //! ```ignore
//! let mut menu = Menu::new(); //! let mut menu = muda::Menu::new();
//! // --snip-- //! // --snip--
//! #[cfg(target_os = "windows")] //! #[cfg(target_os = "windows")]
//! menu.init_for_hwnd(window.hwnd() as isize); //! menu.init_for_hwnd(window.hwnd() as isize);
@ -48,8 +48,8 @@
//! //!
//! You can use [`menu_event_receiver`] to get a reference to the [`MenuEventReceiver`] //! 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 //! which you can use to listen to events when a menu item is activated
//! ```no_run //! ```ignore
//! if let Ok(event) = menu_event_receiver().try_recv() { //! if let Ok(event) = muda::menu_event_receiver().try_recv() {
//! match event.id { //! match event.id {
//! _ if event.id == save_item.id() => { //! _ if event.id == save_item.id() => {
//! println!("Save menu item activated"); //! println!("Save menu item activated");
@ -59,9 +59,11 @@
//! } //! }
//! ``` //! ```
use accelerator::Accelerator;
use crossbeam_channel::{unbounded, Receiver, Sender}; use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
pub mod accelerator;
mod counter; mod counter;
mod platform_impl; mod platform_impl;
@ -85,8 +87,8 @@ pub struct MenuEvent {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```no_run
/// let mut menu = Menu::new(); /// let mut menu = muda::Menu::new();
/// let file_menu = menu.add_submenu("File", true); /// let file_menu = menu.add_submenu("File", true);
/// let edit_menu = menu.add_submenu("Edit", true); /// let edit_menu = menu.add_submenu("Edit", true);
/// ``` /// ```
@ -278,7 +280,7 @@ impl Submenu {
&mut self, &mut self,
label: S, label: S,
enabled: bool, enabled: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> MenuItem { ) -> MenuItem {
MenuItem(self.0.add_item(label, enabled, accelerator)) MenuItem(self.0.add_item(label, enabled, accelerator))
} }
@ -294,7 +296,7 @@ impl Submenu {
label: S, label: S,
enabled: bool, enabled: bool,
checked: bool, checked: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> CheckMenuItem { ) -> CheckMenuItem {
CheckMenuItem(self.0.add_check_item(label, enabled, checked, accelerator)) CheckMenuItem(self.0.add_check_item(label, enabled, checked, accelerator))
} }

View file

@ -1,3 +1,8 @@
use gtk::{prelude::*, AccelGroup};
use keyboard_types::{Code, Modifiers};
use crate::accelerator::Accelerator;
pub fn to_gtk_menemenoic<S: AsRef<str>>(string: S) -> String { pub fn to_gtk_menemenoic<S: AsRef<str>>(string: S) -> String {
string string
.as_ref() .as_ref()
@ -6,39 +11,168 @@ pub fn to_gtk_menemenoic<S: AsRef<str>>(string: S) -> String {
.replace("[~~]", "&&") .replace("[~~]", "&&")
} }
pub fn to_gtk_accelerator<S: AsRef<str>>(accelerator: S) -> String { pub fn register_accelerator<M: IsA<gtk::Widget>>(
let accelerator = accelerator.as_ref(); item: &M,
let mut s = accelerator.split("+"); accel_group: &AccelGroup,
let count = s.clone().count(); menu_key: &Accelerator,
let (mod1, mod2, key) = { ) {
if count == 2 { let accel_key = match &menu_key.key {
(s.next().unwrap(), None, s.next().unwrap()) Code::KeyA => 'A' as u32,
} else if count == 3 { Code::KeyB => 'B' as u32,
( Code::KeyC => 'C' as u32,
s.next().unwrap(), Code::KeyD => 'D' as u32,
Some(s.next().unwrap()), Code::KeyE => 'E' as u32,
s.next().unwrap(), Code::KeyF => 'F' as u32,
) Code::KeyG => 'G' as u32,
Code::KeyH => 'H' as u32,
Code::KeyI => 'I' as u32,
Code::KeyJ => 'J' as u32,
Code::KeyK => 'K' as u32,
Code::KeyL => 'L' as u32,
Code::KeyM => 'M' as u32,
Code::KeyN => 'N' as u32,
Code::KeyO => 'O' as u32,
Code::KeyP => 'P' as u32,
Code::KeyQ => 'Q' as u32,
Code::KeyR => 'R' as u32,
Code::KeyS => 'S' as u32,
Code::KeyT => 'T' as u32,
Code::KeyU => 'U' as u32,
Code::KeyV => 'V' as u32,
Code::KeyW => 'W' as u32,
Code::KeyX => 'X' as u32,
Code::KeyY => 'Y' as u32,
Code::KeyZ => 'Z' as u32,
Code::Digit0 => '0' as u32,
Code::Digit1 => '1' as u32,
Code::Digit2 => '2' as u32,
Code::Digit3 => '3' as u32,
Code::Digit4 => '4' as u32,
Code::Digit5 => '5' as u32,
Code::Digit6 => '6' as u32,
Code::Digit7 => '7' as u32,
Code::Digit8 => '8' as u32,
Code::Digit9 => '9' as u32,
Code::Comma => ',' as u32,
Code::Minus => '-' as u32,
Code::Period => '.' as u32,
Code::Space => ' ' as u32,
Code::Equal => '=' as u32,
Code::Semicolon => ';' as u32,
Code::Slash => '/' as u32,
Code::Backslash => '\\' as u32,
Code::Quote => '\'' as u32,
Code::Backquote => '`' as u32,
Code::BracketLeft => '[' as u32,
Code::BracketRight => ']' as u32,
k => {
if let Some(gdk_key) = key_to_raw_key(k) {
*gdk_key
} else { } else {
panic!("Unsupported accelerator format: {}", accelerator) dbg!("Cannot map key {:?}", k);
return;
}
} }
}; };
let mut gtk_accelerator = parse_mod(mod1).to_string(); item.add_accelerator(
if let Some(mod2) = mod2 { "activate",
gtk_accelerator.push_str(parse_mod(mod2)); accel_group,
} accel_key,
gtk_accelerator.push_str(key); modifiers_to_gdk_modifier_type(menu_key.mods),
gtk::AccelFlags::VISIBLE,
gtk_accelerator );
} }
fn parse_mod(modifier: &str) -> &str { fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType {
match modifier.to_uppercase().as_str() { let mut result = gdk::ModifierType::empty();
"SHIFT" => "<Shift>",
"CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => "<Ctrl>", result.set(
"ALT" => "<Alt>", gdk::ModifierType::MOD1_MASK,
"SUPER" | "META" | "WIN" => "<Meta>", modifiers.contains(Modifiers::ALT),
_ => panic!("Unsupported modifier: {}", modifier), );
} result.set(
gdk::ModifierType::CONTROL_MASK,
modifiers.contains(Modifiers::CONTROL),
);
result.set(
gdk::ModifierType::SHIFT_MASK,
modifiers.contains(Modifiers::SHIFT),
);
result.set(
gdk::ModifierType::META_MASK,
modifiers.contains(Modifiers::SUPER),
);
result
}
fn key_to_raw_key(src: &Code) -> Option<gdk::keys::Key> {
use gdk::keys::constants::*;
Some(match src {
Code::Escape => Escape,
Code::Backspace => BackSpace,
Code::Tab => Tab,
Code::Enter => Return,
Code::ControlLeft => Control_L,
Code::AltLeft => Alt_L,
Code::ShiftLeft => Shift_L,
Code::MetaLeft => Super_L,
Code::ControlRight => Control_R,
Code::AltRight => Alt_R,
Code::ShiftRight => Shift_R,
Code::MetaRight => Super_R,
Code::CapsLock => Caps_Lock,
Code::F1 => F1,
Code::F2 => F2,
Code::F3 => F3,
Code::F4 => F4,
Code::F5 => F5,
Code::F6 => F6,
Code::F7 => F7,
Code::F8 => F8,
Code::F9 => F9,
Code::F10 => F10,
Code::F11 => F11,
Code::F12 => F12,
Code::F13 => F13,
Code::F14 => F14,
Code::F15 => F15,
Code::F16 => F16,
Code::F17 => F17,
Code::F18 => F18,
Code::F19 => F19,
Code::F20 => F20,
Code::F21 => F21,
Code::F22 => F22,
Code::F23 => F23,
Code::F24 => F24,
Code::PrintScreen => Print,
Code::ScrollLock => Scroll_Lock,
// Pause/Break not audio.
Code::Pause => Pause,
Code::Insert => Insert,
Code::Delete => Delete,
Code::Home => Home,
Code::End => End,
Code::PageUp => Page_Up,
Code::PageDown => Page_Down,
Code::NumLock => Num_Lock,
Code::ArrowUp => Up,
Code::ArrowDown => Down,
Code::ArrowLeft => Left,
Code::ArrowRight => Right,
Code::ContextMenu => Menu,
Code::WakeUp => WakeUp,
_ => return None,
})
} }

View file

@ -1,7 +1,7 @@
mod accelerator; mod accelerator;
use crate::{counter::Counter, NativeMenuItem}; use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem};
use accelerator::{to_gtk_accelerator, to_gtk_menemenoic}; use accelerator::{register_accelerator, to_gtk_menemenoic};
use gtk::{prelude::*, Orientation}; use gtk::{prelude::*, Orientation};
use std::{cell::RefCell, collections::HashMap, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
@ -14,7 +14,7 @@ struct MenuEntry {
enabled: bool, enabled: bool,
checked: bool, checked: bool,
id: u64, id: u64,
accelerator: Option<String>, accelerator: Option<Accelerator>,
r#type: MenuEntryType, r#type: MenuEntryType,
entries: Option<Vec<Rc<RefCell<MenuEntry>>>>, entries: Option<Vec<Rc<RefCell<MenuEntry>>>>,
} }
@ -247,7 +247,7 @@ impl Submenu {
&mut self, &mut self,
label: S, label: S,
enabled: bool, enabled: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> MenuItem { ) -> MenuItem {
let label = label.as_ref().to_string(); let label = label.as_ref().to_string();
let id = COUNTER.next(); let id = COUNTER.next();
@ -257,7 +257,7 @@ impl Submenu {
enabled, enabled,
r#type: MenuEntryType::MenuItem(Vec::new()), r#type: MenuEntryType::MenuItem(Vec::new()),
id, id,
accelerator: accelerator.map(|s| s.to_string()), accelerator: accelerator.clone(),
..Default::default() ..Default::default()
})); }));
@ -265,13 +265,7 @@ impl Submenu {
if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type { if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type {
for (_, menu) in native_menus { for (_, menu) in native_menus {
let item = create_gtk_menu_item( let item = create_gtk_menu_item(&label, enabled, &accelerator, id, &*self.1);
&label,
enabled,
&accelerator.map(|s| s.to_string()),
id,
&*self.1,
);
menu.append(&item); menu.append(&item);
if let MenuEntryType::MenuItem(native_items) = &mut entry.borrow_mut().r#type { if let MenuEntryType::MenuItem(native_items) = &mut entry.borrow_mut().r#type {
native_items.push(item); native_items.push(item);
@ -304,7 +298,7 @@ impl Submenu {
label: S, label: S,
enabled: bool, enabled: bool,
checked: bool, checked: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> CheckMenuItem { ) -> CheckMenuItem {
let label = label.as_ref().to_string(); let label = label.as_ref().to_string();
let id = COUNTER.next(); let id = COUNTER.next();
@ -315,7 +309,7 @@ impl Submenu {
checked, checked,
r#type: MenuEntryType::CheckMenuItem(Vec::new()), r#type: MenuEntryType::CheckMenuItem(Vec::new()),
id, id,
accelerator: accelerator.map(|s| s.to_string()), accelerator: accelerator.clone(),
..Default::default() ..Default::default()
})); }));
@ -327,7 +321,7 @@ impl Submenu {
&label, &label,
enabled, enabled,
checked, checked,
&accelerator.map(|s| s.to_string()), &accelerator,
id, id,
&*self.1, &*self.1,
); );
@ -511,21 +505,14 @@ fn create_gtk_submenu(label: &str, enabled: bool) -> (gtk::MenuItem, gtk::Menu)
fn create_gtk_menu_item( fn create_gtk_menu_item(
label: &str, label: &str,
enabled: bool, enabled: bool,
accelerator: &Option<String>, accelerator: &Option<Accelerator>,
id: u64, id: u64,
accel_group: &gtk::AccelGroup, accel_group: &gtk::AccelGroup,
) -> gtk::MenuItem { ) -> gtk::MenuItem {
let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label)); let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label));
item.set_sensitive(enabled); item.set_sensitive(enabled);
if let Some(accelerator) = accelerator { if let Some(accelerator) = accelerator {
let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); register_accelerator(&item, accel_group, accelerator);
item.add_accelerator(
"activate",
accel_group,
key,
modifiers,
gtk::AccelFlags::VISIBLE,
);
} }
item.connect_activate(move |_| { item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
@ -538,7 +525,7 @@ fn create_gtk_check_menu_item(
label: &str, label: &str,
enabled: bool, enabled: bool,
checked: bool, checked: bool,
accelerator: &Option<String>, accelerator: &Option<Accelerator>,
id: u64, id: u64,
accel_group: &gtk::AccelGroup, accel_group: &gtk::AccelGroup,
) -> gtk::CheckMenuItem { ) -> gtk::CheckMenuItem {
@ -546,14 +533,7 @@ fn create_gtk_check_menu_item(
item.set_sensitive(enabled); item.set_sensitive(enabled);
item.set_active(checked); item.set_active(checked);
if let Some(accelerator) = accelerator { if let Some(accelerator) = accelerator {
let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator)); register_accelerator(&item, accel_group, accelerator);
item.add_accelerator(
"activate",
accel_group,
key,
modifiers,
gtk::AccelFlags::VISIBLE,
);
} }
item.connect_activate(move |_| { item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id }); let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });

View file

@ -1,108 +1,133 @@
use cocoa::appkit::NSEventModifierFlags; use cocoa::appkit::NSEventModifierFlags;
use keyboard_types::{Code, Modifiers};
use crate::accelerator::Accelerator;
/// Mnemonic is deprecated since macOS 10 /// Mnemonic is deprecated since macOS 10
pub fn remove_mnemonic(string: impl AsRef<str>) -> String { pub fn remove_mnemonic(string: impl AsRef<str>) -> String {
string.as_ref().replace("&", "") string.as_ref().replace("&", "")
} }
/// Returns a tuple of (Key, Modifier) impl Accelerator {
pub fn parse_accelerator(accelerator: impl AsRef<str>) -> (String, NSEventModifierFlags) { /// Return the string value of this hotkey, for use with Cocoa `NSResponder`
let accelerator = accelerator.as_ref(); /// objects.
let mut s = accelerator.split("+"); ///
let count = s.clone().count(); /// Returns the empty string if no key equivalent is known.
let (mod1, mod2, key) = { pub fn key_equivalent(&self) -> String {
if count == 2 { match self.key {
(s.next().unwrap(), None, s.next().unwrap()) Code::KeyA => "a".into(),
} else if count == 3 { Code::KeyB => "b".into(),
( Code::KeyC => "c".into(),
s.next().unwrap(), Code::KeyD => "d".into(),
Some(s.next().unwrap()), Code::KeyE => "e".into(),
s.next().unwrap(), Code::KeyF => "f".into(),
) Code::KeyG => "g".into(),
} else { Code::KeyH => "h".into(),
panic!("Unsupported accelerator format: {}", accelerator) Code::KeyI => "i".into(),
Code::KeyJ => "j".into(),
Code::KeyK => "k".into(),
Code::KeyL => "l".into(),
Code::KeyM => "m".into(),
Code::KeyN => "n".into(),
Code::KeyO => "o".into(),
Code::KeyP => "p".into(),
Code::KeyQ => "q".into(),
Code::KeyR => "r".into(),
Code::KeyS => "s".into(),
Code::KeyT => "t".into(),
Code::KeyU => "u".into(),
Code::KeyV => "v".into(),
Code::KeyW => "w".into(),
Code::KeyX => "x".into(),
Code::KeyY => "y".into(),
Code::KeyZ => "z".into(),
Code::Digit0 => "0".into(),
Code::Digit1 => "1".into(),
Code::Digit2 => "2".into(),
Code::Digit3 => "3".into(),
Code::Digit4 => "4".into(),
Code::Digit5 => "5".into(),
Code::Digit6 => "6".into(),
Code::Digit7 => "7".into(),
Code::Digit8 => "8".into(),
Code::Digit9 => "9".into(),
Code::Comma => ",".into(),
Code::Minus => "-".into(),
Code::Period => ".".into(),
Code::Space => "\u{0020}".into(),
Code::Equal => "=".into(),
Code::Semicolon => ";".into(),
Code::Slash => "/".into(),
Code::Backslash => "\\".into(),
Code::Quote => "\'".into(),
Code::Backquote => "`".into(),
Code::BracketLeft => "[".into(),
Code::BracketRight => "]".into(),
Code::Tab => "".into(),
Code::Escape => "\u{001b}".into(),
// from NSText.h
Code::Enter => "\u{0003}".into(),
Code::Backspace => "\u{0008}".into(),
Code::Delete => "\u{007f}".into(),
// from NSEvent.h
Code::Insert => "\u{F727}".into(),
Code::Home => "\u{F729}".into(),
Code::End => "\u{F72B}".into(),
Code::PageUp => "\u{F72C}".into(),
Code::PageDown => "\u{F72D}".into(),
Code::PrintScreen => "\u{F72E}".into(),
Code::ScrollLock => "\u{F72F}".into(),
Code::ArrowUp => "\u{F700}".into(),
Code::ArrowDown => "\u{F701}".into(),
Code::ArrowLeft => "\u{F702}".into(),
Code::ArrowRight => "\u{F703}".into(),
Code::F1 => "\u{F704}".into(),
Code::F2 => "\u{F705}".into(),
Code::F3 => "\u{F706}".into(),
Code::F4 => "\u{F707}".into(),
Code::F5 => "\u{F708}".into(),
Code::F6 => "\u{F709}".into(),
Code::F7 => "\u{F70A}".into(),
Code::F8 => "\u{F70B}".into(),
Code::F9 => "\u{F70C}".into(),
Code::F10 => "\u{F70D}".into(),
Code::F11 => "\u{F70E}".into(),
Code::F12 => "\u{F70F}".into(),
Code::F13 => "\u{F710}".into(),
Code::F14 => "\u{F711}".into(),
Code::F15 => "\u{F712}".into(),
Code::F16 => "\u{F713}".into(),
Code::F17 => "\u{F714}".into(),
Code::F18 => "\u{F715}".into(),
Code::F19 => "\u{F716}".into(),
Code::F20 => "\u{F717}".into(),
Code::F21 => "\u{F718}".into(),
Code::F22 => "\u{F719}".into(),
Code::F23 => "\u{F71A}".into(),
Code::F24 => "\u{F71B}".into(),
_ => {
#[cfg(debug_assertions)]
eprintln!("no key equivalent for {:?}", self);
"".into()
}
} }
};
let mut mods = NSEventModifierFlags::empty();
let mod1_flag = parse_mod(mod1);
mods |= mod1_flag;
if let Some(mod2) = mod2 {
let mod2_flag = parse_mod(mod2);
mods |= mod2_flag;
} }
let key_equivalent = parse_key(key); pub fn key_modifier_mask(&self) -> NSEventModifierFlags {
let mods: Modifiers = self.mods;
(key_equivalent, mods) let mut flags = NSEventModifierFlags::empty();
} if mods.contains(Modifiers::SHIFT) {
flags.insert(NSEventModifierFlags::NSShiftKeyMask);
fn parse_mod(modifier: &str) -> NSEventModifierFlags { }
match modifier.to_uppercase().as_str() { if mods.contains(Modifiers::SUPER) {
"SHIFT" => NSEventModifierFlags::NSShiftKeyMask, flags.insert(NSEventModifierFlags::NSCommandKeyMask);
"CONTROL" | "CTRL" => NSEventModifierFlags::NSControlKeyMask, }
"OPTION" | "ALT" => NSEventModifierFlags::NSAlternateKeyMask, if mods.contains(Modifiers::ALT) {
"COMMAND" | "CMD" | "SUPER" | "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" flags.insert(NSEventModifierFlags::NSAlternateKeyMask);
| "CMDORCONTROL" => NSEventModifierFlags::NSCommandKeyMask, }
_ => panic!("Unsupported modifier: {}", modifier), if mods.contains(Modifiers::CONTROL) {
} flags.insert(NSEventModifierFlags::NSControlKeyMask);
} }
flags
fn parse_key(key: &str) -> String {
match key.to_uppercase().as_str() {
"SPACE" => "\u{0020}".into(),
"BACKSPACE" => "\u{0008}".into(),
"TAB" => "".into(),
"ENTER" | "RETURN" => "\u{0003}".into(),
"ESC" | "ESCAPE" => "\u{001b}".into(),
"PAGEUP" => "\u{F72C}".into(),
"PAGEDOWN" => "\u{F72D}".into(),
"END" => "\u{F72B}".into(),
"HOME" => "\u{F729}".into(),
"LEFTARROW" => "\u{F702}".into(),
"UPARROW" => "\u{F700}".into(),
"RIGHTARROW" => "\u{F703}".into(),
"DOWNARROW" => "\u{F701}".into(),
"DELETE" => "\u{007f}".into(),
"0" => "0".into(),
"1" => "1".into(),
"2" => "2".into(),
"3" => "3".into(),
"4" => "4".into(),
"5" => "5".into(),
"6" => "6".into(),
"7" => "7".into(),
"8" => "8".into(),
"9" => "9".into(),
"A" => "a".into(),
"B" => "b".into(),
"C" => "c".into(),
"D" => "d".into(),
"E" => "e".into(),
"F" => "f".into(),
"G" => "g".into(),
"H" => "h".into(),
"I" => "i".into(),
"J" => "j".into(),
"K" => "k".into(),
"L" => "l".into(),
"M" => "m".into(),
"N" => "n".into(),
"O" => "o".into(),
"P" => "p".into(),
"Q" => "q".into(),
"R" => "r".into(),
"S" => "s".into(),
"T" => "t".into(),
"U" => "u".into(),
"V" => "v".into(),
"W" => "w".into(),
"X" => "x".into(),
"Y" => "y".into(),
"Z" => "z".into(),
"," => ",".into(),
"." => ".".into(),
"/" => "/".into(),
_ => panic!("Unsupported modifier: {}", key),
} }
} }

View file

@ -1,5 +1,6 @@
use crate::accelerator::Accelerator;
use crate::counter::Counter; use crate::counter::Counter;
use crate::platform_impl::platform_impl::accelerator::{parse_accelerator, remove_mnemonic}; use crate::platform_impl::platform_impl::accelerator::remove_mnemonic;
use cocoa::{ use cocoa::{
appkit::{NSButton, NSEventModifierFlags, NSMenuItem}, appkit::{NSButton, NSEventModifierFlags, NSMenuItem},
base::{id, nil, BOOL, NO, YES}, base::{id, nil, BOOL, NO, YES},
@ -29,7 +30,7 @@ impl MenuItem {
label: S, label: S,
enabled: bool, enabled: bool,
selector: Sel, selector: Sel,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> Self { ) -> Self {
let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator);
@ -95,7 +96,7 @@ impl CheckMenuItem {
enabled: bool, enabled: bool,
checked: bool, checked: bool,
selector: Sel, selector: Sel,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> Self { ) -> Self {
let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator); let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator);
@ -169,7 +170,11 @@ impl CheckMenuItem {
} }
} }
pub fn make_menu_item(title: &str, selector: Sel, accelerator: Option<&str>) -> (u64, *mut Object) { pub fn make_menu_item(
title: &str,
selector: Sel,
accelerator: Option<Accelerator>,
) -> (u64, *mut Object) {
let alloc = make_menu_item_alloc(); let alloc = make_menu_item_alloc();
let menu_id = COUNTER.next(); let menu_id = COUNTER.next();
@ -221,12 +226,13 @@ fn make_menu_item_from_alloc(
alloc: *mut Object, alloc: *mut Object,
title: *mut Object, title: *mut Object,
selector: Sel, selector: Sel,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> *mut Object { ) -> *mut Object {
unsafe { unsafe {
let (key_equivalent, masks) = match accelerator { let (key_equivalent, masks) = match accelerator {
Some(accelerator) => { Some(accelerator) => {
let (key, mods) = parse_accelerator(accelerator); let key = accelerator.key_equivalent();
let mods = accelerator.key_modifier_mask();
let key = NSString::alloc(nil).init_str(&key); let key = NSString::alloc(nil).init_str(&key);
(key, mods) (key, mods)
} }

View file

@ -1,13 +1,15 @@
mod accelerator; mod accelerator;
mod menu_item; mod menu_item;
use crate::platform_impl::platform_impl::menu_item::make_menu_item; use crate::accelerator::{RawMods, SysMods};
use crate::NativeMenuItem; use crate::NativeMenuItem;
use crate::{accelerator::Accelerator, platform_impl::platform_impl::menu_item::make_menu_item};
use cocoa::{ use cocoa::{
appkit::{NSApp, NSApplication, NSMenu, NSMenuItem}, appkit::{NSApp, NSApplication, NSMenu, NSMenuItem},
base::{id, nil, selector, NO}, base::{id, nil, selector, NO},
foundation::{NSAutoreleasePool, NSString}, foundation::{NSAutoreleasePool, NSString},
}; };
use keyboard_types::Code;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use self::accelerator::remove_mnemonic; use self::accelerator::remove_mnemonic;
@ -90,7 +92,7 @@ impl Submenu {
&mut self, &mut self,
label: S, label: S,
enabled: bool, enabled: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> MenuItem { ) -> MenuItem {
let item = MenuItem::new(label, enabled, sel!(fireMenubarAction:), accelerator); let item = MenuItem::new(label, enabled, sel!(fireMenubarAction:), accelerator);
unsafe { unsafe {
@ -110,17 +112,25 @@ impl Submenu {
None, None,
) )
} }
NativeMenuItem::CloseWindow => { NativeMenuItem::CloseWindow => make_menu_item(
make_menu_item("Close Window", selector("performClose:"), Some("Command+W")) "Close Window",
} selector("performClose:"),
NativeMenuItem::Quit => { Some(Accelerator::new(SysMods::Cmd, Code::KeyW)),
make_menu_item("Quit", selector("terminate:"), Some("Command+Q")) ),
} NativeMenuItem::Quit => make_menu_item(
NativeMenuItem::Hide => make_menu_item("Hide", selector("hide:"), Some("Command+H")), "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( NativeMenuItem::HideOthers => make_menu_item(
"Hide Others", "Hide Others",
selector("hideOtherApplications:"), selector("hideOtherApplications:"),
Some("Alt+H"), Some(Accelerator::new(RawMods::Alt, Code::KeyH)),
), ),
NativeMenuItem::ShowAll => { NativeMenuItem::ShowAll => {
make_menu_item("Show All", selector("unhideAllApplications:"), None) make_menu_item("Show All", selector("unhideAllApplications:"), None)
@ -128,24 +138,44 @@ impl Submenu {
NativeMenuItem::ToggleFullScreen => make_menu_item( NativeMenuItem::ToggleFullScreen => make_menu_item(
"Toggle Full Screen", "Toggle Full Screen",
selector("toggleFullScreen:"), selector("toggleFullScreen:"),
Some("Ctrl+F"), Some(Accelerator::new(RawMods::Ctrl, Code::KeyF)),
), ),
NativeMenuItem::Minimize => make_menu_item( NativeMenuItem::Minimize => make_menu_item(
"Minimize", "Minimize",
selector("performMiniaturize:"), selector("performMiniaturize:"),
Some("Command+M"), Some(Accelerator::new(SysMods::Cmd, Code::KeyM)),
), ),
NativeMenuItem::Zoom => make_menu_item("Zoom", selector("performZoom:"), None), NativeMenuItem::Zoom => make_menu_item("Zoom", selector("performZoom:"), None),
NativeMenuItem::Copy => make_menu_item("Copy", selector("copy:"), Some("Command+C")), NativeMenuItem::Copy => make_menu_item(
NativeMenuItem::Cut => make_menu_item("Cut", selector("cut:"), Some("Command+X")), "Copy",
NativeMenuItem::Paste => make_menu_item("Paste", selector("paste:"), Some("Command+V")), selector("copy:"),
NativeMenuItem::Undo => make_menu_item("Undo", selector("undo:"), Some("Command+Z")), Some(Accelerator::new(SysMods::Cmd, Code::KeyC)),
NativeMenuItem::Redo => { ),
make_menu_item("Redo", selector("redo:"), Some("Command+Shift+Z")) NativeMenuItem::Cut => make_menu_item(
} "Cut",
NativeMenuItem::SelectAll => { selector("cut:"),
make_menu_item("Select All", selector("selectAll:"), Some("Command+A")) 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 { NativeMenuItem::Services => unsafe {
let (_, item) = make_menu_item("Services", sel!(fireMenubarAction:), None); let (_, item) = make_menu_item("Services", sel!(fireMenubarAction:), None);
let app_class = class!(NSApplication); let app_class = class!(NSApplication);
@ -165,7 +195,7 @@ impl Submenu {
label: S, label: S,
enabled: bool, enabled: bool,
checked: bool, checked: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> CheckMenuItem { ) -> CheckMenuItem {
let item = CheckMenuItem::new( let item = CheckMenuItem::new(
label, label,

View file

@ -1,115 +1,243 @@
use windows_sys::Win32::UI::WindowsAndMessaging::{FALT, FCONTROL, FSHIFT, FVIRTKEY}; use std::fmt;
/// Returns a tuple of (Key, Modifier, a string representation to be used in menu items) use keyboard_types::{Code, Modifiers};
pub fn parse_accelerator<S: AsRef<str>>(accelerator: S) -> (u16, u32, String) { use windows_sys::Win32::UI::{
let accelerator = accelerator.as_ref(); Input::KeyboardAndMouse::*,
let mut s = accelerator.split("+"); WindowsAndMessaging::{ACCEL, FALT, FCONTROL, FSHIFT, FVIRTKEY},
let count = s.clone().count(); };
let (mod1, mod2, key) = {
if count == 2 { use crate::accelerator::Accelerator;
(s.next().unwrap(), None, s.next().unwrap())
} else if count == 3 { impl Accelerator {
( // Convert a hotkey to an accelerator.
s.next().unwrap(), pub fn to_accel(&self, menu_id: u16) -> ACCEL {
Some(s.next().unwrap()), let mut virt_key = FVIRTKEY;
s.next().unwrap(), let key_mods: Modifiers = self.mods;
) if key_mods.contains(Modifiers::CONTROL) {
} else { virt_key |= FCONTROL;
panic!("Unsupported accelerator format: {}", accelerator)
} }
}; if key_mods.contains(Modifiers::ALT) {
virt_key |= FALT;
let mut accel_str = String::new(); }
let mut mods_vk = FVIRTKEY; if key_mods.contains(Modifiers::SHIFT) {
virt_key |= FSHIFT;
let (mod1_vk, mod1_str) = parse_mod(mod1);
accel_str.push_str(mod1_str);
accel_str.push_str("+");
mods_vk |= mod1_vk;
if let Some(mod2) = mod2 {
let (mod2_vk, mod2_str) = parse_mod(mod2);
accel_str.push_str(mod2_str);
accel_str.push_str("+");
mods_vk |= mod2_vk;
} }
let (key_vk, key_str) = parse_key(key);
accel_str.push_str(key_str);
(key_vk, mods_vk, accel_str) let vk_code = key_to_vk(&self.key);
} let mod_code = vk_code >> 8;
if mod_code & 0x1 != 0 {
virt_key |= FSHIFT;
}
if mod_code & 0x02 != 0 {
virt_key |= FCONTROL;
}
if mod_code & 0x04 != 0 {
virt_key |= FALT;
}
let raw_key = vk_code & 0x00ff;
fn parse_mod(modifier: &str) -> (u32, &str) { ACCEL {
match modifier.to_uppercase().as_str() { fVirt: virt_key as u8,
"SHIFT" => (FSHIFT, "Shift"), key: raw_key as u16,
"CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => (FCONTROL, "Ctrl"), cmd: menu_id,
"ALT" => (FALT, "Alt"), }
_ => panic!("Unsupported modifier: {}", modifier),
} }
} }
fn parse_key(key: &str) -> (u16, &str) { // used to build accelerators table from Key
match key.to_uppercase().as_str() { fn key_to_vk(key: &Code) -> VIRTUAL_KEY {
"SPACE" => (0x20, "Space"), match key {
"BACKSPACE" => (0x08, "Backspace"), Code::KeyA => unsafe { VkKeyScanW('a' as u16) as u16 },
"TAB" => (0x09, "Tab"), Code::KeyB => unsafe { VkKeyScanW('b' as u16) as u16 },
"ENTER" | "RETURN" => (0x0D, "Enter"), Code::KeyC => unsafe { VkKeyScanW('c' as u16) as u16 },
"CAPSLOCK" => (0x14, "Caps Lock"), Code::KeyD => unsafe { VkKeyScanW('d' as u16) as u16 },
"ESC" | "ESCAPE" => (0x1B, "Esc"), Code::KeyE => unsafe { VkKeyScanW('e' as u16) as u16 },
"PAGEUP" => (0x21, "Page Up"), Code::KeyF => unsafe { VkKeyScanW('f' as u16) as u16 },
"PAGEDOWN" => (0x22, "Page Down"), Code::KeyG => unsafe { VkKeyScanW('g' as u16) as u16 },
"END" => (0x23, "End"), Code::KeyH => unsafe { VkKeyScanW('h' as u16) as u16 },
"HOME" => (0x24, "Home"), Code::KeyI => unsafe { VkKeyScanW('i' as u16) as u16 },
"LEFTARROW" => (0x25, "Left Arrow"), Code::KeyJ => unsafe { VkKeyScanW('j' as u16) as u16 },
"UPARROW" => (0x26, "Up Arrow"), Code::KeyK => unsafe { VkKeyScanW('k' as u16) as u16 },
"RIGHTARROW" => (0x27, "Right Arrow"), Code::KeyL => unsafe { VkKeyScanW('l' as u16) as u16 },
"DOWNARROW" => (0x28, "Down Arrow"), Code::KeyM => unsafe { VkKeyScanW('m' as u16) as u16 },
"DELETE" => (0x2E, "Del"), Code::KeyN => unsafe { VkKeyScanW('n' as u16) as u16 },
"0" => (0x30, "0"), Code::KeyO => unsafe { VkKeyScanW('o' as u16) as u16 },
"1" => (0x31, "1"), Code::KeyP => unsafe { VkKeyScanW('p' as u16) as u16 },
"2" => (0x32, "2"), Code::KeyQ => unsafe { VkKeyScanW('q' as u16) as u16 },
"3" => (0x33, "3"), Code::KeyR => unsafe { VkKeyScanW('r' as u16) as u16 },
"4" => (0x34, "4"), Code::KeyS => unsafe { VkKeyScanW('s' as u16) as u16 },
"5" => (0x35, "5"), Code::KeyT => unsafe { VkKeyScanW('t' as u16) as u16 },
"6" => (0x36, "6"), Code::KeyU => unsafe { VkKeyScanW('u' as u16) as u16 },
"7" => (0x37, "7"), Code::KeyV => unsafe { VkKeyScanW('v' as u16) as u16 },
"8" => (0x38, "8"), Code::KeyW => unsafe { VkKeyScanW('w' as u16) as u16 },
"9" => (0x39, "9"), Code::KeyX => unsafe { VkKeyScanW('x' as u16) as u16 },
"A" => (0x41, "A"), Code::KeyY => unsafe { VkKeyScanW('y' as u16) as u16 },
"B" => (0x42, "B"), Code::KeyZ => unsafe { VkKeyScanW('z' as u16) as u16 },
"C" => (0x43, "C"), Code::Digit0 => unsafe { VkKeyScanW('0' as u16) as u16 },
"D" => (0x44, "D"), Code::Digit1 => unsafe { VkKeyScanW('1' as u16) as u16 },
"E" => (0x45, "E"), Code::Digit2 => unsafe { VkKeyScanW('2' as u16) as u16 },
"F" => (0x46, "F"), Code::Digit3 => unsafe { VkKeyScanW('3' as u16) as u16 },
"G" => (0x47, "G"), Code::Digit4 => unsafe { VkKeyScanW('4' as u16) as u16 },
"H" => (0x48, "H"), Code::Digit5 => unsafe { VkKeyScanW('5' as u16) as u16 },
"I" => (0x49, "I"), Code::Digit6 => unsafe { VkKeyScanW('6' as u16) as u16 },
"J" => (0x4A, "J"), Code::Digit7 => unsafe { VkKeyScanW('7' as u16) as u16 },
"K" => (0x4B, "K"), Code::Digit8 => unsafe { VkKeyScanW('8' as u16) as u16 },
"L" => (0x4C, "L"), Code::Digit9 => unsafe { VkKeyScanW('9' as u16) as u16 },
"M" => (0x4D, "M"), Code::Comma => VK_OEM_COMMA,
"N" => (0x4E, "N"), Code::Minus => VK_OEM_MINUS,
"O" => (0x4F, "O"), Code::Period => VK_OEM_PERIOD,
"P" => (0x50, "P"), Code::Equal => unsafe { VkKeyScanW('=' as u16) as u16 },
"Q" => (0x51, "Q"), Code::Semicolon => unsafe { VkKeyScanW(';' as u16) as u16 },
"R" => (0x52, "R"), Code::Slash => unsafe { VkKeyScanW('/' as u16) as u16 },
"S" => (0x53, "S"), Code::Backslash => unsafe { VkKeyScanW('\\' as u16) as u16 },
"T" => (0x54, "T"), Code::Quote => unsafe { VkKeyScanW('\'' as u16) as u16 },
"U" => (0x55, "U"), Code::Backquote => unsafe { VkKeyScanW('`' as u16) as u16 },
"V" => (0x56, "V"), Code::BracketLeft => unsafe { VkKeyScanW('[' as u16) as u16 },
"W" => (0x57, "W"), Code::BracketRight => unsafe { VkKeyScanW(']' as u16) as u16 },
"X" => (0x58, "X"), Code::Backspace => VK_BACK,
"Y" => (0x59, "Y"), Code::Tab => VK_TAB,
"Z" => (0x5A, "Z"), Code::Space => VK_SPACE,
"NUM0" | "NUMPAD0" => (0x60, "Num 0"), Code::Enter => VK_RETURN,
"NUM1" | "NUMPAD1" => (0x61, "Num 1"), Code::Pause => VK_PAUSE,
"NUM2" | "NUMPAD2" => (0x62, "Num 2"), Code::CapsLock => VK_CAPITAL,
"NUM3" | "NUMPAD3" => (0x63, "Num 3"), Code::KanaMode => VK_KANA,
"NUM4" | "NUMPAD4" => (0x64, "Num 4"), Code::Escape => VK_ESCAPE,
"NUM5" | "NUMPAD5" => (0x65, "Num 5"), Code::NonConvert => VK_NONCONVERT,
"NUM6" | "NUMPAD6" => (0x66, "Num 6"), Code::PageUp => VK_PRIOR,
"NUM7" | "NUMPAD7" => (0x67, "Num 7"), Code::PageDown => VK_NEXT,
"NUM8" | "NUMPAD8" => (0x68, "Num 8"), Code::End => VK_END,
"NUM9" | "NUMPAD9" => (0x69, "Num 9"), Code::Home => VK_HOME,
_ => panic!("Unsupported modifier: {}", key), Code::ArrowLeft => VK_LEFT,
Code::ArrowUp => VK_UP,
Code::ArrowRight => VK_RIGHT,
Code::ArrowDown => VK_DOWN,
Code::PrintScreen => VK_SNAPSHOT,
Code::Insert => VK_INSERT,
Code::Delete => VK_DELETE,
Code::Help => VK_HELP,
Code::ContextMenu => VK_APPS,
Code::F1 => VK_F1,
Code::F2 => VK_F2,
Code::F3 => VK_F3,
Code::F4 => VK_F4,
Code::F5 => VK_F5,
Code::F6 => VK_F6,
Code::F7 => VK_F7,
Code::F8 => VK_F8,
Code::F9 => VK_F9,
Code::F10 => VK_F10,
Code::F11 => VK_F11,
Code::F12 => VK_F12,
Code::F13 => VK_F13,
Code::F14 => VK_F14,
Code::F15 => VK_F15,
Code::F16 => VK_F16,
Code::F17 => VK_F17,
Code::F18 => VK_F18,
Code::F19 => VK_F19,
Code::F20 => VK_F20,
Code::F21 => VK_F21,
Code::F22 => VK_F22,
Code::F23 => VK_F23,
Code::F24 => VK_F24,
Code::NumLock => VK_NUMLOCK,
Code::ScrollLock => VK_SCROLL,
Code::BrowserBack => VK_BROWSER_BACK,
Code::BrowserForward => VK_BROWSER_FORWARD,
Code::BrowserRefresh => VK_BROWSER_REFRESH,
Code::BrowserStop => VK_BROWSER_STOP,
Code::BrowserSearch => VK_BROWSER_SEARCH,
Code::BrowserFavorites => VK_BROWSER_FAVORITES,
Code::BrowserHome => VK_BROWSER_HOME,
Code::AudioVolumeMute => VK_VOLUME_MUTE,
Code::AudioVolumeDown => VK_VOLUME_DOWN,
Code::AudioVolumeUp => VK_VOLUME_UP,
Code::MediaTrackNext => VK_MEDIA_NEXT_TRACK,
Code::MediaTrackPrevious => VK_MEDIA_PREV_TRACK,
Code::MediaStop => VK_MEDIA_STOP,
Code::MediaPlayPause => VK_MEDIA_PLAY_PAUSE,
Code::LaunchMail => VK_LAUNCH_MAIL,
Code::Convert => VK_CONVERT,
key => panic!("Unsupported modifier: {}", key),
}
}
impl fmt::Display for Accelerator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let key_mods: Modifiers = self.mods;
if key_mods.contains(Modifiers::CONTROL) {
write!(f, "Ctrl+")?;
}
if key_mods.contains(Modifiers::SHIFT) {
write!(f, "Shift+")?;
}
if key_mods.contains(Modifiers::ALT) {
write!(f, "Alt+")?;
}
if key_mods.contains(Modifiers::SUPER) {
write!(f, "Windows+")?;
}
match &self.key {
Code::KeyA => write!(f, "A"),
Code::KeyB => write!(f, "B"),
Code::KeyC => write!(f, "C"),
Code::KeyD => write!(f, "D"),
Code::KeyE => write!(f, "E"),
Code::KeyF => write!(f, "F"),
Code::KeyG => write!(f, "G"),
Code::KeyH => write!(f, "H"),
Code::KeyI => write!(f, "I"),
Code::KeyJ => write!(f, "J"),
Code::KeyK => write!(f, "K"),
Code::KeyL => write!(f, "L"),
Code::KeyM => write!(f, "M"),
Code::KeyN => write!(f, "N"),
Code::KeyO => write!(f, "O"),
Code::KeyP => write!(f, "P"),
Code::KeyQ => write!(f, "Q"),
Code::KeyR => write!(f, "R"),
Code::KeyS => write!(f, "S"),
Code::KeyT => write!(f, "T"),
Code::KeyU => write!(f, "U"),
Code::KeyV => write!(f, "V"),
Code::KeyW => write!(f, "W"),
Code::KeyX => write!(f, "X"),
Code::KeyY => write!(f, "Y"),
Code::KeyZ => write!(f, "Z"),
Code::Digit0 => write!(f, "0"),
Code::Digit1 => write!(f, "1"),
Code::Digit2 => write!(f, "2"),
Code::Digit3 => write!(f, "3"),
Code::Digit4 => write!(f, "4"),
Code::Digit5 => write!(f, "5"),
Code::Digit6 => write!(f, "6"),
Code::Digit7 => write!(f, "7"),
Code::Digit8 => write!(f, "8"),
Code::Digit9 => write!(f, "9"),
Code::Comma => write!(f, ","),
Code::Minus => write!(f, "-"),
Code::Period => write!(f, "."),
Code::Space => write!(f, "Space"),
Code::Equal => write!(f, "="),
Code::Semicolon => write!(f, ";"),
Code::Slash => write!(f, "/"),
Code::Backslash => write!(f, "\\"),
Code::Quote => write!(f, "\'"),
Code::Backquote => write!(f, "`"),
Code::BracketLeft => write!(f, "["),
Code::BracketRight => write!(f, "]"),
Code::Tab => write!(f, "Tab"),
Code::Escape => write!(f, "Esc"),
Code::Delete => write!(f, "Del"),
Code::Insert => write!(f, "Ins"),
Code::PageUp => write!(f, "PgUp"),
Code::PageDown => write!(f, "PgDn"),
// These names match LibreOffice.
Code::ArrowLeft => write!(f, "Left"),
Code::ArrowRight => write!(f, "Right"),
Code::ArrowUp => write!(f, "Up"),
Code::ArrowDown => write!(f, "Down"),
_ => write!(f, "{:?}", self.key),
}
} }
} }

View file

@ -3,7 +3,7 @@
mod accelerator; mod accelerator;
mod util; mod util;
use crate::{counter::Counter, NativeMenuItem}; use crate::{accelerator::Accelerator, counter::Counter, NativeMenuItem};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{cell::RefCell, collections::HashMap, rc::Rc}; use std::{cell::RefCell, collections::HashMap, rc::Rc};
use util::{decode_wide, encode_wide, LOWORD}; use util::{decode_wide, encode_wide, LOWORD};
@ -23,8 +23,6 @@ use windows_sys::Win32::{
}, },
}; };
use self::accelerator::parse_accelerator;
const COUNTER_START: u64 = 1000; const COUNTER_START: u64 = 1000;
static COUNTER: Counter = Counter::new_with_start(COUNTER_START); static COUNTER: Counter = Counter::new_with_start(COUNTER_START);
@ -195,7 +193,7 @@ impl Submenu {
&mut self, &mut self,
label: S, label: S,
enabled: bool, enabled: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> MenuItem { ) -> MenuItem {
let id = COUNTER.next(); let id = COUNTER.next();
let mut flags = MF_STRING; let mut flags = MF_STRING;
@ -205,12 +203,8 @@ impl Submenu {
let mut label = label.as_ref().to_string(); let mut label = label.as_ref().to_string();
if let Some(accelerator) = accelerator { if let Some(accelerator) = accelerator {
let (key, mods, accel_str) = parse_accelerator(accelerator); let accel_str = accelerator.to_string();
let accel = ACCEL { let accel = accelerator.to_accel(id as u16);
key,
fVirt: mods as _,
cmd: id as _,
};
label.push_str("\t"); label.push_str("\t");
label.push_str(&accel_str); label.push_str(&accel_str);
@ -268,7 +262,7 @@ impl Submenu {
label: S, label: S,
enabled: bool, enabled: bool,
checked: bool, checked: bool,
accelerator: Option<&str>, accelerator: Option<Accelerator>,
) -> CheckMenuItem { ) -> CheckMenuItem {
let mut item = CheckMenuItem(self.add_item(label, enabled, accelerator)); let mut item = CheckMenuItem(self.add_item(label, enabled, accelerator));
item.set_checked(checked); item.set_checked(checked);