mirror of
https://github.com/italicsjenga/muda.git
synced 2024-12-24 04:11:30 +11:00
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:
parent
28ffd206fa
commit
e33c5f0daf
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
288
src/accelerator.rs
Normal 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());
|
||||||
|
}
|
36
src/lib.rs
36
src/lib.rs
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: >k::AccelGroup,
|
accel_group: >k::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: >k::AccelGroup,
|
accel_group: >k::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 });
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
||||||
(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 {
|
pub fn key_modifier_mask(&self) -> NSEventModifierFlags {
|
||||||
match key.to_uppercase().as_str() {
|
let mods: Modifiers = self.mods;
|
||||||
"SPACE" => "\u{0020}".into(),
|
let mut flags = NSEventModifierFlags::empty();
|
||||||
"BACKSPACE" => "\u{0008}".into(),
|
if mods.contains(Modifiers::SHIFT) {
|
||||||
"TAB" => "⇥".into(),
|
flags.insert(NSEventModifierFlags::NSShiftKeyMask);
|
||||||
"ENTER" | "RETURN" => "\u{0003}".into(),
|
}
|
||||||
"ESC" | "ESCAPE" => "\u{001b}".into(),
|
if mods.contains(Modifiers::SUPER) {
|
||||||
"PAGEUP" => "\u{F72C}".into(),
|
flags.insert(NSEventModifierFlags::NSCommandKeyMask);
|
||||||
"PAGEDOWN" => "\u{F72D}".into(),
|
}
|
||||||
"END" => "\u{F72B}".into(),
|
if mods.contains(Modifiers::ALT) {
|
||||||
"HOME" => "\u{F729}".into(),
|
flags.insert(NSEventModifierFlags::NSAlternateKeyMask);
|
||||||
"LEFTARROW" => "\u{F702}".into(),
|
}
|
||||||
"UPARROW" => "\u{F700}".into(),
|
if mods.contains(Modifiers::CONTROL) {
|
||||||
"RIGHTARROW" => "\u{F703}".into(),
|
flags.insert(NSEventModifierFlags::NSControlKeyMask);
|
||||||
"DOWNARROW" => "\u{F701}".into(),
|
}
|
||||||
"DELETE" => "\u{007f}".into(),
|
flags
|
||||||
"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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
|
||||||
(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)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut accel_str = String::new();
|
use crate::accelerator::Accelerator;
|
||||||
let mut mods_vk = FVIRTKEY;
|
|
||||||
|
|
||||||
let (mod1_vk, mod1_str) = parse_mod(mod1);
|
impl Accelerator {
|
||||||
accel_str.push_str(mod1_str);
|
// Convert a hotkey to an accelerator.
|
||||||
accel_str.push_str("+");
|
pub fn to_accel(&self, menu_id: u16) -> ACCEL {
|
||||||
mods_vk |= mod1_vk;
|
let mut virt_key = FVIRTKEY;
|
||||||
if let Some(mod2) = mod2 {
|
let key_mods: Modifiers = self.mods;
|
||||||
let (mod2_vk, mod2_str) = parse_mod(mod2);
|
if key_mods.contains(Modifiers::CONTROL) {
|
||||||
accel_str.push_str(mod2_str);
|
virt_key |= FCONTROL;
|
||||||
accel_str.push_str("+");
|
|
||||||
mods_vk |= mod2_vk;
|
|
||||||
}
|
}
|
||||||
let (key_vk, key_str) = parse_key(key);
|
if key_mods.contains(Modifiers::ALT) {
|
||||||
accel_str.push_str(key_str);
|
virt_key |= FALT;
|
||||||
|
}
|
||||||
(key_vk, mods_vk, accel_str)
|
if key_mods.contains(Modifiers::SHIFT) {
|
||||||
|
virt_key |= FSHIFT;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_mod(modifier: &str) -> (u32, &str) {
|
let vk_code = key_to_vk(&self.key);
|
||||||
match modifier.to_uppercase().as_str() {
|
let mod_code = vk_code >> 8;
|
||||||
"SHIFT" => (FSHIFT, "Shift"),
|
if mod_code & 0x1 != 0 {
|
||||||
"CONTROL" | "CTRL" | "COMMAND" | "COMMANDORCONTROL" | "COMMANDORCTRL" => (FCONTROL, "Ctrl"),
|
virt_key |= FSHIFT;
|
||||||
"ALT" => (FALT, "Alt"),
|
}
|
||||||
_ => panic!("Unsupported modifier: {}", modifier),
|
if mod_code & 0x02 != 0 {
|
||||||
|
virt_key |= FCONTROL;
|
||||||
|
}
|
||||||
|
if mod_code & 0x04 != 0 {
|
||||||
|
virt_key |= FALT;
|
||||||
|
}
|
||||||
|
let raw_key = vk_code & 0x00ff;
|
||||||
|
|
||||||
|
ACCEL {
|
||||||
|
fVirt: virt_key as u8,
|
||||||
|
key: raw_key as u16,
|
||||||
|
cmd: menu_id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue