mirror of
https://github.com/tauri-apps/global-hotkey.git
synced 2026-01-31 00:45:22 +01:00
Add Windows implementation
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/target
|
||||
/.vscode
|
||||
2062
Cargo.lock
generated
2062
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -3,14 +3,31 @@ name = "global-hotkey"
|
||||
version = "0.0.0"
|
||||
description = "Global hotkeys for Desktop Applications"
|
||||
edition = "2021"
|
||||
keywords = [ "windowing", "global", "global-hotkey", "hotkey" ]
|
||||
keywords = ["windowing", "global", "global-hotkey", "hotkey"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/amrbashir/global-hotkey"
|
||||
documentation = "https://docs.rs/global-hotkey"
|
||||
categories = [ "gui" ]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
categories = ["gui"]
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel = "0.5"
|
||||
keyboard-types = "0.6"
|
||||
once_cell = "1.16.0"
|
||||
thiserror = "1.0"
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
|
||||
version = "0.42"
|
||||
features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_Foundation",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
]
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
winit = "0.27"
|
||||
tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }
|
||||
|
||||
37
examples/tao.rs
Normal file
37
examples/tao.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use global_hotkey::{
|
||||
global_hotkey_event_receiver,
|
||||
hotkey::{Code, HotKey, Modifiers},
|
||||
GlobalHotKeyManager,
|
||||
};
|
||||
use tao::event_loop::{ControlFlow, EventLoopBuilder};
|
||||
|
||||
fn main() {
|
||||
let event_loop = EventLoopBuilder::new().build();
|
||||
|
||||
let hotkeys_manager = GlobalHotKeyManager::new().unwrap();
|
||||
|
||||
let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD);
|
||||
let hotkey2 = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyD);
|
||||
|
||||
hotkeys_manager.register(hotkey).unwrap();
|
||||
|
||||
hotkeys_manager.register(hotkey2).unwrap();
|
||||
|
||||
let global_hotkey_channel = global_hotkey_event_receiver();
|
||||
|
||||
event_loop.run(move |_event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Poll;
|
||||
|
||||
if let Ok(event) = global_hotkey_channel.try_recv() {
|
||||
println!("{:?}", event);
|
||||
|
||||
if hotkey2.id() == event.id() {
|
||||
hotkeys_manager.unregister(hotkey2).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
37
examples/winit.rs
Normal file
37
examples/winit.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use global_hotkey::{
|
||||
global_hotkey_event_receiver,
|
||||
hotkey::{Code, HotKey, Modifiers},
|
||||
GlobalHotKeyManager,
|
||||
};
|
||||
use winit::event_loop::{ControlFlow, EventLoopBuilder};
|
||||
|
||||
fn main() {
|
||||
let event_loop = EventLoopBuilder::new().build();
|
||||
|
||||
let hotkeys_manager = GlobalHotKeyManager::new().unwrap();
|
||||
|
||||
let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD);
|
||||
let hotkey2 = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyD);
|
||||
|
||||
hotkeys_manager.register(hotkey).unwrap();
|
||||
|
||||
hotkeys_manager.register(hotkey2).unwrap();
|
||||
|
||||
let global_hotkey_channel = global_hotkey_event_receiver();
|
||||
|
||||
event_loop.run(move |_event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Poll;
|
||||
|
||||
if let Ok(event) = global_hotkey_channel.try_recv() {
|
||||
println!("{:?}", event);
|
||||
|
||||
if hotkey2.id() == event.id() {
|
||||
hotkeys_manager.unregister(hotkey2).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
17
src/counter.rs
Normal file
17
src/counter.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
pub struct Counter(AtomicU32);
|
||||
|
||||
impl Counter {
|
||||
pub const fn new() -> Self {
|
||||
Self(AtomicU32::new(1))
|
||||
}
|
||||
|
||||
pub fn next(&self) -> u32 {
|
||||
self.0.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
22
src/error.rs
Normal file
22
src/error.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors returned by tray-icon.
|
||||
#[non_exhaustive]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
OsError(#[from] std::io::Error),
|
||||
#[error("{0}")]
|
||||
HotKeyParseError(String),
|
||||
#[error("{0}")]
|
||||
FailedToRegister(String),
|
||||
#[error("Failed to unregister this hotkey")]
|
||||
FailedToUnRegister,
|
||||
}
|
||||
|
||||
/// Convenient type alias of Result type for tray-icon.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
254
src/hotkey.rs
Normal file
254
src/hotkey.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! HotKeys describe keyboard global shortcuts.
|
||||
//!
|
||||
//! [`HotKey`s](crate::hotkey::HotKey) are used to define a keyboard shortcut consisting
|
||||
//! of an optional combination of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and
|
||||
//! one key ([`Code`](crate::hotkey::Code)).
|
||||
//!
|
||||
//! # Examples
|
||||
//! They can be created directly
|
||||
//! ```no_run
|
||||
//! # use global_hotkey::hotkey::{HotKey, Modifiers, Code};
|
||||
//! let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyQ);
|
||||
//! let hotkey_without_mods = HotKey::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 global_hotkey::hotkey::{HotKey};
|
||||
//! let hotkey: HotKey = "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::<HotKey>().is_err());
|
||||
//! ```
|
||||
//!
|
||||
|
||||
pub use keyboard_types::{Code, Modifiers};
|
||||
use std::{borrow::Borrow, hash::Hash, str::FromStr};
|
||||
|
||||
use crate::counter::Counter;
|
||||
|
||||
static COUNTER: Counter = Counter::new();
|
||||
|
||||
/// A keyboard shortcut that consists of an optional combination
|
||||
/// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and
|
||||
/// one key ([`Code`](crate::hotkey::Code)).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
|
||||
pub struct HotKey {
|
||||
pub(crate) mods: Modifiers,
|
||||
pub(crate) key: Code,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
impl HotKey {
|
||||
/// Creates a new hotkey 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,
|
||||
id: COUNTER.next(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the id associated with this HotKey
|
||||
pub fn id(&self) -> u32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Returns `true` if this [`Code`] and [`Modifiers`] matches this `hotkey`.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// HotKey::from_str is available to be backward
|
||||
// compatible with tauri and it also open the option
|
||||
// to generate hotkey from string
|
||||
impl FromStr for HotKey {
|
||||
type Err = crate::Error;
|
||||
fn from_str(hotkey_string: &str) -> Result<Self, Self::Err> {
|
||||
parse_hotkey(hotkey_string)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hotkey(hotkey_string: &str) -> crate::Result<HotKey> {
|
||||
let mut mods = Modifiers::empty();
|
||||
let mut key = Code::Unidentified;
|
||||
|
||||
let mut split = hotkey_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::HotKeyParseError(format!(
|
||||
"Couldn't identify \"{}\" as a valid `Code`",
|
||||
token
|
||||
))),
|
||||
_ => Ok(code),
|
||||
}
|
||||
} else {
|
||||
Err(crate::Error::HotKeyParseError(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 split {
|
||||
let token = raw.trim().to_string();
|
||||
if token.is_empty() {
|
||||
return Err(crate::Error::HotKeyParseError(
|
||||
"Unexpected empty token while parsing hotkey".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::HotKeyParseError(format!(
|
||||
"Unexpected hotkey string format: \"{}\"",
|
||||
hotkey_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(HotKey {
|
||||
key,
|
||||
mods,
|
||||
id: COUNTER.next(),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_hotkey() {
|
||||
assert_eq!(
|
||||
parse_hotkey("KeyX").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::empty(),
|
||||
key: Code::KeyX,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("CTRL+KeyX").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::CONTROL,
|
||||
key: Code::KeyX,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("SHIFT+KeyC").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::SHIFT,
|
||||
key: Code::KeyC,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("CTRL+KeyZ").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::CONTROL,
|
||||
key: Code::KeyZ,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("super+ctrl+SHIFT+alt+ArrowUp").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::META | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
|
||||
key: Code::ArrowUp,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("Digit5").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::empty(),
|
||||
key: Code::Digit5,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("KeyG").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::empty(),
|
||||
key: Code::KeyG,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let hotkey = parse_hotkey("+G");
|
||||
assert!(hotkey.is_err());
|
||||
|
||||
let hotkey = parse_hotkey("SHGSH+G");
|
||||
assert!(hotkey.is_err());
|
||||
|
||||
assert_eq!(
|
||||
parse_hotkey("SHiFT+F12").unwrap(),
|
||||
HotKey {
|
||||
mods: Modifiers::SHIFT,
|
||||
key: Code::F12,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_hotkey("CmdOrCtrl+Space").unwrap(),
|
||||
HotKey {
|
||||
#[cfg(target_os = "macos")]
|
||||
mods: Modifiers::META,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mods: Modifiers::CONTROL,
|
||||
key: Code::Space,
|
||||
id: 0,
|
||||
}
|
||||
);
|
||||
|
||||
let hotkey = parse_hotkey("CTRL+");
|
||||
assert!(hotkey.is_err());
|
||||
}
|
||||
70
src/lib.rs
70
src/lib.rs
@@ -1 +1,71 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
mod counter;
|
||||
mod error;
|
||||
pub mod hotkey;
|
||||
mod platform_impl;
|
||||
|
||||
pub use self::error::*;
|
||||
use hotkey::HotKey;
|
||||
|
||||
/// Contains the id of the triggered [`HotKey`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct GlobalHotKeyEvent(pub u32);
|
||||
|
||||
impl GlobalHotKeyEvent {
|
||||
/// Returns the id contained in this event
|
||||
pub fn id(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A reciever that could be used to listen to tray events.
|
||||
pub type GlobalHotKeyEventReceiver = Receiver<GlobalHotKeyEvent>;
|
||||
|
||||
static GLOBAL_HOTKEY_CHANNEL: Lazy<(Sender<GlobalHotKeyEvent>, GlobalHotKeyEventReceiver)> =
|
||||
Lazy::new(unbounded);
|
||||
|
||||
/// Gets a reference to the event channel's [TrayEventReceiver]
|
||||
/// which can be used to listen for tray events.
|
||||
pub fn global_hotkey_event_receiver<'a>() -> &'a GlobalHotKeyEventReceiver {
|
||||
&GLOBAL_HOTKEY_CHANNEL.1
|
||||
}
|
||||
|
||||
pub struct GlobalHotKeyManager {
|
||||
platform_impl: platform_impl::GlobalHotKeyManager,
|
||||
}
|
||||
|
||||
impl GlobalHotKeyManager {
|
||||
pub fn new() -> crate::Result<Self> {
|
||||
Ok(Self {
|
||||
platform_impl: platform_impl::GlobalHotKeyManager::new()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register(&self, hotkey: HotKey) -> crate::Result<()> {
|
||||
self.platform_impl.register(hotkey)
|
||||
}
|
||||
|
||||
pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> {
|
||||
self.platform_impl.unregister(hotkey)
|
||||
}
|
||||
|
||||
pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> {
|
||||
for hotkey in hotkeys {
|
||||
self.register(*hotkey)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> {
|
||||
for hotkey in hotkeys {
|
||||
self.register(*hotkey)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
16
src/platform_impl/macos/ffi.rs
Normal file
16
src/platform_impl/macos/ffi.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use std::ffi::c_void;
|
||||
|
||||
pub type EventRef = *const c_void;
|
||||
|
||||
#[link(name = "Carbon", kind = "framework")]
|
||||
extern "C" {
|
||||
pub fn GetEventParameter(
|
||||
inEvent: isize,
|
||||
inName: isize,
|
||||
inDesiredType: isize,
|
||||
outActualType: *mut c_void,
|
||||
inBufferSize: isize,
|
||||
outActualSize: *mut c_void,
|
||||
outData: *mut c_void,
|
||||
);
|
||||
}
|
||||
9
src/platform_impl/macos/mod.rs
Normal file
9
src/platform_impl/macos/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod ffi;
|
||||
|
||||
pub struct GlobalHotKeyManager {}
|
||||
|
||||
impl GlobalHotKeyManager {
|
||||
pub fn init() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
15
src/platform_impl/mod.rs
Normal file
15
src/platform_impl/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/mod.rs"]
|
||||
mod platform;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "gtk/mod.rs"]
|
||||
mod platform;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "macos/mod.rs"]
|
||||
mod platform;
|
||||
|
||||
pub(crate) use self::platform::*;
|
||||
299
src/platform_impl/windows/mod.rs
Normal file
299
src/platform_impl/windows/mod.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::ptr;
|
||||
|
||||
use keyboard_types::{Code, Modifiers};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{HWND, LPARAM, LRESULT, WPARAM},
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
RegisterHotKey, UnregisterHotKey, VkKeyScanW, MOD_ALT, MOD_CONTROL, MOD_NOREPEAT,
|
||||
MOD_SHIFT, MOD_WIN, VIRTUAL_KEY, VK_APPS, VK_BACK, VK_BROWSER_BACK,
|
||||
VK_BROWSER_FAVORITES, VK_BROWSER_FORWARD, VK_BROWSER_HOME, VK_BROWSER_REFRESH,
|
||||
VK_BROWSER_SEARCH, VK_BROWSER_STOP, VK_CAPITAL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE,
|
||||
VK_F1, VK_F10, VK_F11, VK_F12, VK_F13, VK_F14, VK_F15, VK_F16, VK_F17, VK_F18, VK_F19,
|
||||
VK_F2, VK_F20, VK_F21, VK_F22, VK_F23, VK_F24, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7,
|
||||
VK_F8, VK_F9, VK_HELP, VK_HOME, VK_INSERT, VK_KANA, VK_LAUNCH_MAIL, VK_LEFT,
|
||||
VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK, VK_MEDIA_STOP, VK_NEXT,
|
||||
VK_NONCONVERT, VK_NUMLOCK, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_PAUSE,
|
||||
VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SCROLL, VK_SNAPSHOT, VK_SPACE, VK_TAB, VK_UP,
|
||||
VK_VOLUME_DOWN, VK_VOLUME_MUTE, VK_VOLUME_UP,
|
||||
},
|
||||
Shell::{DefSubclassProc, SetWindowSubclass},
|
||||
WindowsAndMessaging::{
|
||||
CreateWindowExW, DefWindowProcW, RegisterClassW, CW_USEDEFAULT, HMENU, WM_HOTKEY,
|
||||
WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT,
|
||||
WS_OVERLAPPED,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{hotkey::HotKey, GLOBAL_HOTKEY_CHANNEL};
|
||||
|
||||
const GLOBAL_HOTKEY_SUBCLASS_ID: usize = 6001;
|
||||
|
||||
pub struct GlobalHotKeyManager {
|
||||
hwnd: isize,
|
||||
}
|
||||
|
||||
impl GlobalHotKeyManager {
|
||||
pub fn new() -> crate::Result<Self> {
|
||||
let class_name = encode_wide("tray_icon_app");
|
||||
unsafe {
|
||||
let hinstance = get_instance_handle();
|
||||
|
||||
unsafe extern "system" fn call_default_window_proc(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
DefWindowProcW(hwnd, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
let wnd_class = WNDCLASSW {
|
||||
lpfnWndProc: Some(call_default_window_proc),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
hInstance: hinstance,
|
||||
..std::mem::zeroed()
|
||||
};
|
||||
|
||||
RegisterClassW(&wnd_class);
|
||||
|
||||
let hwnd = CreateWindowExW(
|
||||
WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED |
|
||||
// WS_EX_TOOLWINDOW prevents this window from ever showing up in the taskbar, which
|
||||
// we want to avoid. If you remove this style, this window won't show up in the
|
||||
// taskbar *initially*, but it can show up at some later point. This can sometimes
|
||||
// happen on its own after several hours have passed, although this has proven
|
||||
// difficult to reproduce. Alternatively, it can be manually triggered by killing
|
||||
// `explorer.exe` and then starting the process back up.
|
||||
// It is unclear why the bug is triggered by waiting for several hours.
|
||||
WS_EX_TOOLWINDOW,
|
||||
class_name.as_ptr(),
|
||||
ptr::null(),
|
||||
WS_OVERLAPPED,
|
||||
CW_USEDEFAULT,
|
||||
0,
|
||||
CW_USEDEFAULT,
|
||||
0,
|
||||
HWND::default(),
|
||||
HMENU::default(),
|
||||
hinstance,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
if hwnd == 0 {
|
||||
return Err(crate::Error::OsError(std::io::Error::last_os_error()));
|
||||
}
|
||||
|
||||
SetWindowSubclass(
|
||||
hwnd,
|
||||
Some(global_hotkey_subclass_proc),
|
||||
GLOBAL_HOTKEY_SUBCLASS_ID,
|
||||
0,
|
||||
);
|
||||
|
||||
Ok(Self { hwnd })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(&self, hotkey: HotKey) -> crate::Result<()> {
|
||||
let mut mods = MOD_NOREPEAT;
|
||||
if hotkey.mods.contains(Modifiers::SHIFT) {
|
||||
mods |= MOD_SHIFT;
|
||||
}
|
||||
if hotkey.mods.intersects(Modifiers::SUPER | Modifiers::META) {
|
||||
mods |= MOD_WIN;
|
||||
}
|
||||
if hotkey.mods.contains(Modifiers::ALT) {
|
||||
mods |= MOD_ALT;
|
||||
}
|
||||
if hotkey.mods.contains(Modifiers::CONTROL) {
|
||||
mods |= MOD_CONTROL;
|
||||
}
|
||||
|
||||
// get key scan code
|
||||
match key_to_vk(&hotkey.key) {
|
||||
Some(vk_code) => {
|
||||
let result =
|
||||
unsafe { RegisterHotKey(self.hwnd, hotkey.id() as _, mods, vk_code as _) };
|
||||
if result == 0 {
|
||||
return Err(crate::Error::OsError(std::io::Error::last_os_error()));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(crate::Error::FailedToRegister(format!(
|
||||
"Unable to register accelerator (unknown VKCode for this char: {}).",
|
||||
hotkey.key
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> {
|
||||
let result = unsafe { UnregisterHotKey(self.hwnd, hotkey.id() as _) };
|
||||
if result == 0 {
|
||||
return Err(crate::Error::OsError(std::io::Error::last_os_error()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
unsafe extern "system" fn global_hotkey_subclass_proc(
|
||||
hwnd: HWND,
|
||||
msg: u32,
|
||||
wparam: WPARAM,
|
||||
lparam: LPARAM,
|
||||
_id: usize,
|
||||
_subclass_input_ptr: usize,
|
||||
) -> LRESULT {
|
||||
if msg == WM_HOTKEY {
|
||||
let _ = &GLOBAL_HOTKEY_CHANNEL
|
||||
.0
|
||||
.send(crate::GlobalHotKeyEvent(wparam as _));
|
||||
}
|
||||
|
||||
DefSubclassProc(hwnd, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
pub fn encode_wide<S: AsRef<std::ffi::OsStr>>(string: S) -> Vec<u16> {
|
||||
std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref())
|
||||
.chain(std::iter::once(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_instance_handle() -> windows_sys::Win32::Foundation::HINSTANCE {
|
||||
// Gets the instance handle by taking the address of the
|
||||
// pseudo-variable created by the microsoft linker:
|
||||
// https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483
|
||||
|
||||
// This is preferred over GetModuleHandle(NULL) because it also works in DLLs:
|
||||
// https://stackoverflow.com/questions/21718027/getmodulehandlenull-vs-hinstance
|
||||
|
||||
extern "C" {
|
||||
static __ImageBase: windows_sys::Win32::System::SystemServices::IMAGE_DOS_HEADER;
|
||||
}
|
||||
|
||||
unsafe { &__ImageBase as *const _ as _ }
|
||||
}
|
||||
|
||||
// used to build accelerators table from Key
|
||||
fn key_to_vk(key: &Code) -> Option<VIRTUAL_KEY> {
|
||||
Some(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_INSERT,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user