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]
crossbeam-channel = "0.5"
once_cell = "1.10"
keyboard-types = "0.6"
[target.'cfg(target_os = "windows")'.dependencies.windows-sys]
version = "0.34"
@ -26,6 +27,7 @@ features = [
]
[target.'cfg(target_os = "linux")'.dependencies]
gdk = "0.15"
gtk = "0.15"
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")]
use tao::platform::unix::WindowExtUnix;
#[cfg(target_os = "windows")]
@ -19,7 +23,11 @@ fn main() {
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", 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::CloseWindow);
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")]
use winit::platform::macos::EventLoopBuilderExtMacOS;
#[cfg(target_os = "windows")]
@ -37,7 +41,11 @@ fn main() {
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", 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::CloseWindow);
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.
//! ```no_run
//! let mut menu = Menu::new();
//! let mut menu = muda::Menu::new();
//! ```
//!
//! # Adding submens to the root menu
//!
//! Once you have a root menu you can start adding [`Submenu`]s by using [`Menu::add_submenu`].
//! ```no_run
//! let mut menu = Menu::new();
//! let mut menu = muda::Menu::new();
//! let file_menu = menu.add_submenu("File", 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.
//! ```no_run
//! let mut menu = Menu::new();
//! let mut menu = muda::Menu::new();
//!
//! 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 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 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);
//! 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.
//! ```no_run
//! let mut menu = Menu::new();
//! ```ignore
//! let mut menu = muda::Menu::new();
//! // --snip--
//! #[cfg(target_os = "windows")]
//! 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`]
//! which you can use to listen to events when a menu item is activated
//! ```no_run
//! if let Ok(event) = menu_event_receiver().try_recv() {
//! ```ignore
//! if let Ok(event) = muda::menu_event_receiver().try_recv() {
//! match event.id {
//! _ if event.id == save_item.id() => {
//! println!("Save menu item activated");
@ -59,9 +59,11 @@
//! }
//! ```
use accelerator::Accelerator;
use crossbeam_channel::{unbounded, Receiver, Sender};
use once_cell::sync::Lazy;
pub mod accelerator;
mod counter;
mod platform_impl;
@ -85,8 +87,8 @@ pub struct MenuEvent {
///
/// # Example
///
/// ```
/// let mut menu = Menu::new();
/// ```no_run
/// let mut menu = muda::Menu::new();
/// let file_menu = menu.add_submenu("File", true);
/// let edit_menu = menu.add_submenu("Edit", true);
/// ```
@ -278,7 +280,7 @@ impl Submenu {
&mut self,
label: S,
enabled: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> MenuItem {
MenuItem(self.0.add_item(label, enabled, accelerator))
}
@ -294,7 +296,7 @@ impl Submenu {
label: S,
enabled: bool,
checked: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> CheckMenuItem {
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 {
string
.as_ref()
@ -6,39 +11,168 @@ pub fn to_gtk_menemenoic<S: AsRef<str>>(string: S) -> String {
.replace("[~~]", "&&")
}
pub fn to_gtk_accelerator<S: AsRef<str>>(accelerator: S) -> String {
let accelerator = accelerator.as_ref();
let mut s = accelerator.split("+");
let count = s.clone().count();
let (mod1, mod2, key) = {
if count == 2 {
(s.next().unwrap(), None, s.next().unwrap())
} else if count == 3 {
(
s.next().unwrap(),
Some(s.next().unwrap()),
s.next().unwrap(),
)
} else {
panic!("Unsupported accelerator format: {}", accelerator)
pub fn register_accelerator<M: IsA<gtk::Widget>>(
item: &M,
accel_group: &AccelGroup,
menu_key: &Accelerator,
) {
let accel_key = match &menu_key.key {
Code::KeyA => 'A' as u32,
Code::KeyB => 'B' as u32,
Code::KeyC => 'C' as u32,
Code::KeyD => 'D' as u32,
Code::KeyE => 'E' as u32,
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 {
dbg!("Cannot map key {:?}", k);
return;
}
}
};
let mut gtk_accelerator = parse_mod(mod1).to_string();
if let Some(mod2) = mod2 {
gtk_accelerator.push_str(parse_mod(mod2));
}
gtk_accelerator.push_str(key);
gtk_accelerator
item.add_accelerator(
"activate",
accel_group,
accel_key,
modifiers_to_gdk_modifier_type(menu_key.mods),
gtk::AccelFlags::VISIBLE,
);
}
fn parse_mod(modifier: &str) -> &str {
match modifier.to_uppercase().as_str() {
"SHIFT" => "<Shift>",
"CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => "<Ctrl>",
"ALT" => "<Alt>",
"SUPER" | "META" | "WIN" => "<Meta>",
_ => panic!("Unsupported modifier: {}", modifier),
}
fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType {
let mut result = gdk::ModifierType::empty();
result.set(
gdk::ModifierType::MOD1_MASK,
modifiers.contains(Modifiers::ALT),
);
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;
use crate::{counter::Counter, NativeMenuItem};
use accelerator::{to_gtk_accelerator, to_gtk_menemenoic};
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};
@ -14,7 +14,7 @@ struct MenuEntry {
enabled: bool,
checked: bool,
id: u64,
accelerator: Option<String>,
accelerator: Option<Accelerator>,
r#type: MenuEntryType,
entries: Option<Vec<Rc<RefCell<MenuEntry>>>>,
}
@ -247,7 +247,7 @@ impl Submenu {
&mut self,
label: S,
enabled: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> MenuItem {
let label = label.as_ref().to_string();
let id = COUNTER.next();
@ -257,7 +257,7 @@ impl Submenu {
enabled,
r#type: MenuEntryType::MenuItem(Vec::new()),
id,
accelerator: accelerator.map(|s| s.to_string()),
accelerator: accelerator.clone(),
..Default::default()
}));
@ -265,13 +265,7 @@ impl Submenu {
if let MenuEntryType::Submenu(native_menus) = &mut inner.r#type {
for (_, menu) in native_menus {
let item = create_gtk_menu_item(
&label,
enabled,
&accelerator.map(|s| s.to_string()),
id,
&*self.1,
);
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);
@ -304,7 +298,7 @@ impl Submenu {
label: S,
enabled: bool,
checked: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> CheckMenuItem {
let label = label.as_ref().to_string();
let id = COUNTER.next();
@ -315,7 +309,7 @@ impl Submenu {
checked,
r#type: MenuEntryType::CheckMenuItem(Vec::new()),
id,
accelerator: accelerator.map(|s| s.to_string()),
accelerator: accelerator.clone(),
..Default::default()
}));
@ -327,7 +321,7 @@ impl Submenu {
&label,
enabled,
checked,
&accelerator.map(|s| s.to_string()),
&accelerator,
id,
&*self.1,
);
@ -511,21 +505,14 @@ fn create_gtk_submenu(label: &str, enabled: bool) -> (gtk::MenuItem, gtk::Menu)
fn create_gtk_menu_item(
label: &str,
enabled: bool,
accelerator: &Option<String>,
accelerator: &Option<Accelerator>,
id: u64,
accel_group: &gtk::AccelGroup,
) -> gtk::MenuItem {
let item = gtk::MenuItem::with_mnemonic(&to_gtk_menemenoic(label));
item.set_sensitive(enabled);
if let Some(accelerator) = accelerator {
let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator));
item.add_accelerator(
"activate",
accel_group,
key,
modifiers,
gtk::AccelFlags::VISIBLE,
);
register_accelerator(&item, accel_group, accelerator);
}
item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });
@ -538,7 +525,7 @@ fn create_gtk_check_menu_item(
label: &str,
enabled: bool,
checked: bool,
accelerator: &Option<String>,
accelerator: &Option<Accelerator>,
id: u64,
accel_group: &gtk::AccelGroup,
) -> gtk::CheckMenuItem {
@ -546,14 +533,7 @@ fn create_gtk_check_menu_item(
item.set_sensitive(enabled);
item.set_active(checked);
if let Some(accelerator) = accelerator {
let (key, modifiers) = gtk::accelerator_parse(&to_gtk_accelerator(accelerator));
item.add_accelerator(
"activate",
accel_group,
key,
modifiers,
gtk::AccelFlags::VISIBLE,
);
register_accelerator(&item, accel_group, accelerator);
}
item.connect_activate(move |_| {
let _ = crate::MENU_CHANNEL.0.send(crate::MenuEvent { id });

View file

@ -1,108 +1,133 @@
use cocoa::appkit::NSEventModifierFlags;
use keyboard_types::{Code, Modifiers};
use crate::accelerator::Accelerator;
/// Mnemonic is deprecated since macOS 10
pub fn remove_mnemonic(string: impl AsRef<str>) -> String {
string.as_ref().replace("&", "")
}
/// Returns a tuple of (Key, Modifier)
pub fn parse_accelerator(accelerator: impl AsRef<str>) -> (String, NSEventModifierFlags) {
let accelerator = accelerator.as_ref();
let mut s = accelerator.split("+");
let count = s.clone().count();
let (mod1, mod2, key) = {
if count == 2 {
(s.next().unwrap(), None, s.next().unwrap())
} else if count == 3 {
(
s.next().unwrap(),
Some(s.next().unwrap()),
s.next().unwrap(),
)
} else {
panic!("Unsupported accelerator format: {}", accelerator)
impl Accelerator {
/// Return the string value of this hotkey, for use with Cocoa `NSResponder`
/// objects.
///
/// Returns the empty string if no key equivalent is known.
pub fn key_equivalent(&self) -> String {
match self.key {
Code::KeyA => "a".into(),
Code::KeyB => "b".into(),
Code::KeyC => "c".into(),
Code::KeyD => "d".into(),
Code::KeyE => "e".into(),
Code::KeyF => "f".into(),
Code::KeyG => "g".into(),
Code::KeyH => "h".into(),
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);
(key_equivalent, mods)
}
fn parse_mod(modifier: &str) -> NSEventModifierFlags {
match modifier.to_uppercase().as_str() {
"SHIFT" => NSEventModifierFlags::NSShiftKeyMask,
"CONTROL" | "CTRL" => NSEventModifierFlags::NSControlKeyMask,
"OPTION" | "ALT" => NSEventModifierFlags::NSAlternateKeyMask,
"COMMAND" | "CMD" | "SUPER" | "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL"
| "CMDORCONTROL" => NSEventModifierFlags::NSCommandKeyMask,
_ => panic!("Unsupported modifier: {}", modifier),
}
}
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),
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) {
flags.insert(NSEventModifierFlags::NSCommandKeyMask);
}
if mods.contains(Modifiers::ALT) {
flags.insert(NSEventModifierFlags::NSAlternateKeyMask);
}
if mods.contains(Modifiers::CONTROL) {
flags.insert(NSEventModifierFlags::NSControlKeyMask);
}
flags
}
}

View file

@ -1,5 +1,6 @@
use crate::accelerator::Accelerator;
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::{
appkit::{NSButton, NSEventModifierFlags, NSMenuItem},
base::{id, nil, BOOL, NO, YES},
@ -29,7 +30,7 @@ impl MenuItem {
label: S,
enabled: bool,
selector: Sel,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> Self {
let (id, ns_menu_item) = make_menu_item(&remove_mnemonic(&label), selector, accelerator);
@ -95,7 +96,7 @@ impl CheckMenuItem {
enabled: bool,
checked: bool,
selector: Sel,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> Self {
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 menu_id = COUNTER.next();
@ -221,12 +226,13 @@ fn make_menu_item_from_alloc(
alloc: *mut Object,
title: *mut Object,
selector: Sel,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> *mut Object {
unsafe {
let (key_equivalent, masks) = match 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);
(key, mods)
}

View file

@ -1,13 +1,15 @@
mod accelerator;
mod menu_item;
use crate::platform_impl::platform_impl::menu_item::make_menu_item;
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},
};
use keyboard_types::Code;
use objc::{class, msg_send, sel, sel_impl};
use self::accelerator::remove_mnemonic;
@ -90,7 +92,7 @@ impl Submenu {
&mut self,
label: S,
enabled: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> MenuItem {
let item = MenuItem::new(label, enabled, sel!(fireMenubarAction:), accelerator);
unsafe {
@ -110,17 +112,25 @@ impl Submenu {
None,
)
}
NativeMenuItem::CloseWindow => {
make_menu_item("Close Window", selector("performClose:"), Some("Command+W"))
}
NativeMenuItem::Quit => {
make_menu_item("Quit", selector("terminate:"), Some("Command+Q"))
}
NativeMenuItem::Hide => make_menu_item("Hide", selector("hide:"), Some("Command+H")),
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("Alt+H"),
Some(Accelerator::new(RawMods::Alt, Code::KeyH)),
),
NativeMenuItem::ShowAll => {
make_menu_item("Show All", selector("unhideAllApplications:"), None)
@ -128,24 +138,44 @@ impl Submenu {
NativeMenuItem::ToggleFullScreen => make_menu_item(
"Toggle Full Screen",
selector("toggleFullScreen:"),
Some("Ctrl+F"),
Some(Accelerator::new(RawMods::Ctrl, Code::KeyF)),
),
NativeMenuItem::Minimize => make_menu_item(
"Minimize",
selector("performMiniaturize:"),
Some("Command+M"),
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("Command+C")),
NativeMenuItem::Cut => make_menu_item("Cut", selector("cut:"), Some("Command+X")),
NativeMenuItem::Paste => make_menu_item("Paste", selector("paste:"), Some("Command+V")),
NativeMenuItem::Undo => make_menu_item("Undo", selector("undo:"), Some("Command+Z")),
NativeMenuItem::Redo => {
make_menu_item("Redo", selector("redo:"), Some("Command+Shift+Z"))
}
NativeMenuItem::SelectAll => {
make_menu_item("Select All", selector("selectAll:"), Some("Command+A"))
}
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);
@ -165,7 +195,7 @@ impl Submenu {
label: S,
enabled: bool,
checked: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> CheckMenuItem {
let item = CheckMenuItem::new(
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)
pub fn parse_accelerator<S: AsRef<str>>(accelerator: S) -> (u16, u32, String) {
let accelerator = accelerator.as_ref();
let mut s = accelerator.split("+");
let count = s.clone().count();
let (mod1, mod2, key) = {
if count == 2 {
(s.next().unwrap(), None, s.next().unwrap())
} else if count == 3 {
(
s.next().unwrap(),
Some(s.next().unwrap()),
s.next().unwrap(),
)
} else {
panic!("Unsupported accelerator format: {}", accelerator)
use keyboard_types::{Code, Modifiers};
use windows_sys::Win32::UI::{
Input::KeyboardAndMouse::*,
WindowsAndMessaging::{ACCEL, FALT, FCONTROL, FSHIFT, FVIRTKEY},
};
use crate::accelerator::Accelerator;
impl Accelerator {
// Convert a hotkey to an accelerator.
pub fn to_accel(&self, menu_id: u16) -> ACCEL {
let mut virt_key = FVIRTKEY;
let key_mods: Modifiers = self.mods;
if key_mods.contains(Modifiers::CONTROL) {
virt_key |= FCONTROL;
}
if key_mods.contains(Modifiers::ALT) {
virt_key |= FALT;
}
if key_mods.contains(Modifiers::SHIFT) {
virt_key |= FSHIFT;
}
};
let mut accel_str = String::new();
let mut mods_vk = FVIRTKEY;
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;
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)
}
fn parse_mod(modifier: &str) -> (u32, &str) {
match modifier.to_uppercase().as_str() {
"SHIFT" => (FSHIFT, "Shift"),
"CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => (FCONTROL, "Ctrl"),
"ALT" => (FALT, "Alt"),
_ => panic!("Unsupported modifier: {}", modifier),
ACCEL {
fVirt: virt_key as u8,
key: raw_key as u16,
cmd: menu_id,
}
}
}
fn parse_key(key: &str) -> (u16, &str) {
match key.to_uppercase().as_str() {
"SPACE" => (0x20, "Space"),
"BACKSPACE" => (0x08, "Backspace"),
"TAB" => (0x09, "Tab"),
"ENTER" | "RETURN" => (0x0D, "Enter"),
"CAPSLOCK" => (0x14, "Caps Lock"),
"ESC" | "ESCAPE" => (0x1B, "Esc"),
"PAGEUP" => (0x21, "Page Up"),
"PAGEDOWN" => (0x22, "Page Down"),
"END" => (0x23, "End"),
"HOME" => (0x24, "Home"),
"LEFTARROW" => (0x25, "Left Arrow"),
"UPARROW" => (0x26, "Up Arrow"),
"RIGHTARROW" => (0x27, "Right Arrow"),
"DOWNARROW" => (0x28, "Down Arrow"),
"DELETE" => (0x2E, "Del"),
"0" => (0x30, "0"),
"1" => (0x31, "1"),
"2" => (0x32, "2"),
"3" => (0x33, "3"),
"4" => (0x34, "4"),
"5" => (0x35, "5"),
"6" => (0x36, "6"),
"7" => (0x37, "7"),
"8" => (0x38, "8"),
"9" => (0x39, "9"),
"A" => (0x41, "A"),
"B" => (0x42, "B"),
"C" => (0x43, "C"),
"D" => (0x44, "D"),
"E" => (0x45, "E"),
"F" => (0x46, "F"),
"G" => (0x47, "G"),
"H" => (0x48, "H"),
"I" => (0x49, "I"),
"J" => (0x4A, "J"),
"K" => (0x4B, "K"),
"L" => (0x4C, "L"),
"M" => (0x4D, "M"),
"N" => (0x4E, "N"),
"O" => (0x4F, "O"),
"P" => (0x50, "P"),
"Q" => (0x51, "Q"),
"R" => (0x52, "R"),
"S" => (0x53, "S"),
"T" => (0x54, "T"),
"U" => (0x55, "U"),
"V" => (0x56, "V"),
"W" => (0x57, "W"),
"X" => (0x58, "X"),
"Y" => (0x59, "Y"),
"Z" => (0x5A, "Z"),
"NUM0" | "NUMPAD0" => (0x60, "Num 0"),
"NUM1" | "NUMPAD1" => (0x61, "Num 1"),
"NUM2" | "NUMPAD2" => (0x62, "Num 2"),
"NUM3" | "NUMPAD3" => (0x63, "Num 3"),
"NUM4" | "NUMPAD4" => (0x64, "Num 4"),
"NUM5" | "NUMPAD5" => (0x65, "Num 5"),
"NUM6" | "NUMPAD6" => (0x66, "Num 6"),
"NUM7" | "NUMPAD7" => (0x67, "Num 7"),
"NUM8" | "NUMPAD8" => (0x68, "Num 8"),
"NUM9" | "NUMPAD9" => (0x69, "Num 9"),
_ => panic!("Unsupported modifier: {}", key),
// used to build accelerators table from Key
fn key_to_vk(key: &Code) -> VIRTUAL_KEY {
match key {
Code::KeyA => unsafe { VkKeyScanW('a' as u16) as u16 },
Code::KeyB => unsafe { VkKeyScanW('b' as u16) as u16 },
Code::KeyC => unsafe { VkKeyScanW('c' as u16) as u16 },
Code::KeyD => unsafe { VkKeyScanW('d' as u16) as u16 },
Code::KeyE => unsafe { VkKeyScanW('e' as u16) as u16 },
Code::KeyF => unsafe { VkKeyScanW('f' as u16) as u16 },
Code::KeyG => unsafe { VkKeyScanW('g' as u16) as u16 },
Code::KeyH => unsafe { VkKeyScanW('h' as u16) as u16 },
Code::KeyI => unsafe { VkKeyScanW('i' as u16) as u16 },
Code::KeyJ => unsafe { VkKeyScanW('j' as u16) as u16 },
Code::KeyK => unsafe { VkKeyScanW('k' as u16) as u16 },
Code::KeyL => unsafe { VkKeyScanW('l' as u16) as u16 },
Code::KeyM => unsafe { VkKeyScanW('m' as u16) as u16 },
Code::KeyN => unsafe { VkKeyScanW('n' as u16) as u16 },
Code::KeyO => unsafe { VkKeyScanW('o' as u16) as u16 },
Code::KeyP => unsafe { VkKeyScanW('p' as u16) as u16 },
Code::KeyQ => unsafe { VkKeyScanW('q' as u16) as u16 },
Code::KeyR => unsafe { VkKeyScanW('r' as u16) as u16 },
Code::KeyS => unsafe { VkKeyScanW('s' as u16) as u16 },
Code::KeyT => unsafe { VkKeyScanW('t' as u16) as u16 },
Code::KeyU => unsafe { VkKeyScanW('u' as u16) as u16 },
Code::KeyV => unsafe { VkKeyScanW('v' as u16) as u16 },
Code::KeyW => unsafe { VkKeyScanW('w' as u16) as u16 },
Code::KeyX => unsafe { VkKeyScanW('x' as u16) as u16 },
Code::KeyY => unsafe { VkKeyScanW('y' as u16) as u16 },
Code::KeyZ => unsafe { VkKeyScanW('z' as u16) as u16 },
Code::Digit0 => unsafe { VkKeyScanW('0' as u16) as u16 },
Code::Digit1 => unsafe { VkKeyScanW('1' as u16) as u16 },
Code::Digit2 => unsafe { VkKeyScanW('2' as u16) as u16 },
Code::Digit3 => unsafe { VkKeyScanW('3' as u16) as u16 },
Code::Digit4 => unsafe { VkKeyScanW('4' as u16) as u16 },
Code::Digit5 => unsafe { VkKeyScanW('5' as u16) as u16 },
Code::Digit6 => unsafe { VkKeyScanW('6' as u16) as u16 },
Code::Digit7 => unsafe { VkKeyScanW('7' as u16) as u16 },
Code::Digit8 => unsafe { VkKeyScanW('8' as u16) as u16 },
Code::Digit9 => unsafe { VkKeyScanW('9' as u16) as u16 },
Code::Comma => VK_OEM_COMMA,
Code::Minus => VK_OEM_MINUS,
Code::Period => VK_OEM_PERIOD,
Code::Equal => unsafe { VkKeyScanW('=' as u16) as u16 },
Code::Semicolon => unsafe { VkKeyScanW(';' as u16) as u16 },
Code::Slash => unsafe { VkKeyScanW('/' as u16) as u16 },
Code::Backslash => unsafe { VkKeyScanW('\\' as u16) as u16 },
Code::Quote => unsafe { VkKeyScanW('\'' as u16) as u16 },
Code::Backquote => unsafe { VkKeyScanW('`' as u16) as u16 },
Code::BracketLeft => unsafe { VkKeyScanW('[' as u16) as u16 },
Code::BracketRight => unsafe { VkKeyScanW(']' as u16) as u16 },
Code::Backspace => VK_BACK,
Code::Tab => VK_TAB,
Code::Space => VK_SPACE,
Code::Enter => VK_RETURN,
Code::Pause => VK_PAUSE,
Code::CapsLock => VK_CAPITAL,
Code::KanaMode => VK_KANA,
Code::Escape => VK_ESCAPE,
Code::NonConvert => VK_NONCONVERT,
Code::PageUp => VK_PRIOR,
Code::PageDown => VK_NEXT,
Code::End => VK_END,
Code::Home => VK_HOME,
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 util;
use crate::{counter::Counter, NativeMenuItem};
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};
@ -23,8 +23,6 @@ use windows_sys::Win32::{
},
};
use self::accelerator::parse_accelerator;
const COUNTER_START: u64 = 1000;
static COUNTER: Counter = Counter::new_with_start(COUNTER_START);
@ -195,7 +193,7 @@ impl Submenu {
&mut self,
label: S,
enabled: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> MenuItem {
let id = COUNTER.next();
let mut flags = MF_STRING;
@ -205,12 +203,8 @@ impl Submenu {
let mut label = label.as_ref().to_string();
if let Some(accelerator) = accelerator {
let (key, mods, accel_str) = parse_accelerator(accelerator);
let accel = ACCEL {
key,
fVirt: mods as _,
cmd: id as _,
};
let accel_str = accelerator.to_string();
let accel = accelerator.to_accel(id as u16);
label.push_str("\t");
label.push_str(&accel_str);
@ -268,7 +262,7 @@ impl Submenu {
label: S,
enabled: bool,
checked: bool,
accelerator: Option<&str>,
accelerator: Option<Accelerator>,
) -> CheckMenuItem {
let mut item = CheckMenuItem(self.add_item(label, enabled, accelerator));
item.set_checked(checked);