virtual_desktop_manager\tray_plugins/
hotkeys.rs

1//! Registers hotkeys using the [`global_hotkey`] crate.
2#![cfg(feature = "global_hotkey")]
3
4use crate::{
5    dynamic_gui::DynamicUiHooks,
6    settings::UiSettings,
7    tray::{SystemTray, SystemTrayRef, TrayPlugin, TrayRoot},
8};
9use global_hotkey::{hotkey::HotKey, GlobalHotKeyEvent, GlobalHotKeyManager};
10use std::{
11    any::TypeId,
12    cell::RefCell,
13    collections::HashMap,
14    rc::Rc,
15    sync::{mpsc, Arc, Mutex},
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19enum HotKeyAction {
20    OpenQuickSwitchMenu,
21    OpenContextMenuAtMousePos,
22}
23
24#[derive(Debug, Default)]
25struct CellState {
26    registered_hotkeys: Vec<HotKey>,
27    action_lookup: HashMap<u32, HotKeyAction>,
28}
29impl CellState {
30    pub fn clear(&mut self) {
31        self.registered_hotkeys.clear();
32        self.action_lookup.clear();
33    }
34    pub fn hotkeys(&self) -> &[HotKey] {
35        &self.registered_hotkeys
36    }
37    pub fn add_hotkey(&mut self, hotkey: HotKey, action: HotKeyAction) {
38        self.registered_hotkeys.push(hotkey);
39        self.action_lookup.insert(hotkey.id(), action);
40    }
41}
42
43#[derive(nwd::NwgPartial)]
44pub struct HotKeyPlugin {
45    tray: SystemTrayRef,
46
47    hotkey_manager: GlobalHotKeyManager,
48    current_hotkeys: RefCell<CellState>,
49    events: mpsc::Receiver<GlobalHotKeyEvent>,
50
51    latest_notice_sender: Arc<Mutex<Option<nwg::NoticeSender>>>,
52    /// This notice will be triggered when there are new Virtual Desktop events
53    /// that should be handled.
54    #[nwg_control]
55    #[nwg_events( OnNotice: [Self::on_background_notice] )]
56    background_notice: nwg::Notice,
57}
58impl Default for HotKeyPlugin {
59    fn default() -> Self {
60        let latest_notice_sender = Arc::new(Mutex::new(None::<nwg::NoticeSender>));
61        let (tx, rx) = mpsc::channel();
62        _ = std::thread::Builder::new()
63            .name("GlobalHotKeyListenerThread".to_owned())
64            .spawn({
65                let latest_notice_sender = latest_notice_sender.clone();
66                move || {
67                    let hotkey_rx = GlobalHotKeyEvent::receiver();
68                    for ev in hotkey_rx.iter() {
69                        if tx.send(ev).is_err() {
70                            break;
71                        }
72                        if let Some(sender) = *latest_notice_sender.lock().unwrap() {
73                            sender.notice();
74                        }
75                    }
76                }
77            });
78        Self {
79            tray: Default::default(),
80
81            hotkey_manager: global_hotkey::GlobalHotKeyManager::new()
82                .expect("Failed to create global keyboard shortcut manager"),
83            current_hotkeys: RefCell::default(),
84            events: rx,
85
86            latest_notice_sender,
87            background_notice: Default::default(),
88        }
89    }
90}
91impl DynamicUiHooks<SystemTray> for HotKeyPlugin {
92    fn before_partial_build(
93        &mut self,
94        tray: &Rc<SystemTray>,
95        _should_build: &mut bool,
96    ) -> Option<(nwg::ControlHandle, TypeId)> {
97        self.tray.set(tray);
98        Some((tray.root().window.handle, TypeId::of::<TrayRoot>()))
99    }
100    fn after_partial_build(&mut self, _dynamic_ui: &Rc<SystemTray>) {
101        *self.latest_notice_sender.lock().unwrap() = Some(self.background_notice.sender());
102        self.update_hotkeys();
103    }
104    fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
105        self.background_notice = Default::default();
106    }
107}
108impl TrayPlugin for HotKeyPlugin {
109    fn on_settings_changed(
110        &self,
111        _tray_ui: &Rc<SystemTray>,
112        prev: &Arc<UiSettings>,
113        new: &Arc<UiSettings>,
114    ) {
115        if !Arc::ptr_eq(&prev.quick_switch_hotkey, &new.quick_switch_hotkey)
116            && prev.quick_switch_hotkey != new.quick_switch_hotkey
117        {
118            self.update_hotkeys();
119            return;
120        }
121        if !Arc::ptr_eq(
122            &prev.open_menu_at_mouse_pos_hotkey,
123            &new.open_menu_at_mouse_pos_hotkey,
124        ) && prev.open_menu_at_mouse_pos_hotkey != new.open_menu_at_mouse_pos_hotkey
125        {
126            self.update_hotkeys();
127        }
128    }
129}
130impl HotKeyPlugin {
131    fn on_background_notice(&self) {
132        let Some(tray) = self.tray.get() else {
133            return;
134        };
135        for event in self.events.try_iter() {
136            tracing::debug!(?event, "Received global hotkey");
137            if event.state() == global_hotkey::HotKeyState::Pressed {
138                if let Ok(guard) = self.current_hotkeys.try_borrow() {
139                    let action = guard.action_lookup.get(&event.id()).copied();
140                    drop(guard);
141                    if let Some(action) = action {
142                        match action {
143                            HotKeyAction::OpenQuickSwitchMenu => tray.notify_quick_switch_hotkey(),
144                            HotKeyAction::OpenContextMenuAtMousePos => {
145                                tray.notify_open_menu_at_mouse_position_hotkey()
146                            }
147                        }
148                    } else {
149                        tracing::warn!(?event, "No action registered for the pressed hotkey");
150                    }
151                } else {
152                    tracing::warn!(
153                        ?event,
154                        "Ignored hotkey event because hotkeys were currently being updated"
155                    );
156                }
157            }
158        }
159    }
160    pub fn update_hotkeys(&self) {
161        #[cfg(feature = "global_hotkey")]
162        {
163            let settings = self.tray.get().unwrap().settings().get();
164            let Ok(mut guard) = self.current_hotkeys.try_borrow_mut() else {
165                tracing::warn!("Tried to update global hotkeys recursively");
166                return;
167            };
168            if let Err(e) = self.hotkey_manager.unregister_all(guard.hotkeys()) {
169                tracing::error!(error = e.to_string(), "Failed to unregister global hotkeys");
170            }
171            let mut hotkeys = std::mem::take(&mut *guard);
172            hotkeys.clear();
173
174            if !settings.quick_switch_hotkey.is_empty() {
175                match settings.quick_switch_hotkey.parse() {
176                    Ok(hotkey) => hotkeys.add_hotkey(hotkey, HotKeyAction::OpenQuickSwitchMenu),
177                    Err(e) => {
178                        tracing::warn!(error = e.to_string(), "Invalid quick switch hotkey");
179                    }
180                }
181            }
182            if !settings.open_menu_at_mouse_pos_hotkey.is_empty() {
183                match settings.open_menu_at_mouse_pos_hotkey.parse() {
184                    Ok(hotkey) => {
185                        hotkeys.add_hotkey(hotkey, HotKeyAction::OpenContextMenuAtMousePos)
186                    }
187                    Err(e) => {
188                        tracing::warn!(
189                            error = e.to_string(),
190                            "Invalid hotkey for opening context menu at mouse location"
191                        );
192                    }
193                }
194            }
195
196            tracing::debug!(hotkeys =? hotkeys.hotkeys(), "Registering new hotkeys");
197
198            if hotkeys.hotkeys().is_empty() {
199                *guard = hotkeys;
200                return;
201            }
202            if let Err(e) = self.hotkey_manager.register_all(hotkeys.hotkeys()) {
203                tracing::error!(error = e.to_string(), "Failed to register global hotkeys");
204            } else {
205                *guard = hotkeys;
206            }
207        }
208    }
209}