1
0
Fork 0
mirror of https://github.com/italicsjenga/muda.git synced 2025-02-18 04:57:42 +11:00
muda/src/accelerator.rs
Amr Bashir 0173987ed5
fix: parse one letter string to valid accelerator ()
* fix: parse one letter string to valid accelerator

* clippy
2022-12-20 00:55:57 +02:00

223 lines
7.4 KiB
Rust

// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Accelerators describe keyboard shortcuts for menu items.
//!
//! [`Accelerator`s](crate::accelerator::Accelerator) are used to define a keyboard shortcut consisting
//! of an optional combination of modifier keys (provided by [`Modifiers`](crate::accelerator::Modifiers)) and
//! one key ([`Code`](crate::accelerator::Code)).
//!
//! # Examples
//! They can be created directly
//! ```no_run
//! # use muda::accelerator::{Accelerator, Modifiers, Code};
//! let accelerator = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyQ);
//! let accelerator_without_mods = Accelerator::new(None, Code::KeyQ);
//! ```
//! 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.
//! ```no_run
//! # use muda::accelerator::{Accelerator};
//! 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};
/// A keyboard shortcut that consists of an optional combination
/// of modifier keys (provided by [`Modifiers`](crate::accelerator::Modifiers)) and
/// one key ([`Code`](crate::accelerator::Code)).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
pub struct Accelerator {
pub(crate) mods: Modifiers,
pub(crate) key: Code,
}
impl Accelerator {
/// Creates a new accelerator to define keyboard shortcuts throughout your application.
/// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::META`]/[`Modifiers::SUPER`]
pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
Self {
mods: mods.unwrap_or_else(Modifiers::empty),
key,
}
}
/// Returns `true` if this [`Code`] and [`Modifiers`] matches this `Accelerator`.
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::META
| 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 = crate::Error;
fn from_str(accelerator_string: &str) -> Result<Self, Self::Err> {
parse_accelerator(accelerator_string)
}
}
fn parse_accelerator(accelerator_string: &str) -> crate::Result<Accelerator> {
let mut mods = Modifiers::empty();
let mut key = Code::Unidentified;
let mut split = accelerator_string.split('+');
let len = split.clone().count();
let parse_key = |token: &str| -> crate::Result<Code> {
if let Ok(code) = Code::from_str(token) {
match code {
Code::Unidentified => Err(crate::Error::AcceleratorParseError(format!(
"Couldn't identify \"{}\" as a valid `Code`",
token
))),
_ => Ok(code),
}
} else {
Err(crate::Error::AcceleratorParseError(format!(
"Couldn't identify \"{}\" as a valid `Code`",
token
)))
}
};
if len == 1 {
let token = split.next().unwrap();
key = parse_key(token)?;
} else {
for raw in accelerator_string.split('+') {
let token = raw.trim().to_string();
if token.is_empty() {
return Err(crate::Error::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(crate::Error::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::META, true);
}
"SHIFT" => {
mods.set(Modifiers::SHIFT, true);
}
"COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
#[cfg(target_os = "macos")]
mods.set(Modifiers::META, true);
#[cfg(not(target_os = "macos"))]
mods.set(Modifiers::CONTROL, true);
}
_ => {
key = parse_key(token.as_str())?;
}
}
}
}
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::META | 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::META,
#[cfg(not(target_os = "macos"))]
mods: Modifiers::CONTROL,
key: Code::Space,
}
);
let acc = parse_accelerator("CTRL+");
assert!(acc.is_err());
}