virtual_desktop_manager\tray_plugins/
menus.rs

1use nwg::MenuSeparator;
2
3use crate::{
4    dynamic_gui::{forward_to_dynamic_ui, DynamicUiHooks, DynamicUiWrapper},
5    nwg_ext::menu_remove,
6    settings::{AutoStart, QuickSwitchMenu, TrayIconType, UiSettings},
7    tray::{MenuKeyPressEffect, MenuPosition, SystemTray, SystemTrayRef, TrayPlugin, TrayRoot},
8    vd,
9};
10use std::{
11    any::TypeId,
12    cell::{Cell, RefCell},
13    collections::{BTreeMap, VecDeque},
14    rc::Rc,
15    sync::Arc,
16    time::{Duration, Instant},
17};
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
20pub enum SubMenu {
21    /// Handle for the [`nwg::Menu`] that represents a submenu.
22    Handle(nwg::ControlHandle),
23    /// ASCII character that will select the sub menu.
24    ///
25    /// Note that this won't work if the user is holding the control key when we
26    /// attempt to open the submenu.
27    #[allow(dead_code)]
28    AccessKey(u8),
29}
30impl SubMenu {
31    fn open(self) {
32        let Some(context_menu) = crate::nwg_ext::find_context_menu_window() else {
33            tracing::warn!(wanted_sub_menu =? self, "Failed to find context menu window");
34            return;
35        };
36
37        match self {
38            SubMenu::Handle(control_handle) => {
39                let Some(index) = crate::nwg_ext::menu_index_in_parent(control_handle) else {
40                    tracing::warn!("Failed to find settings submenu");
41                    return;
42                };
43
44                use windows::Win32::{
45                    Foundation::{LPARAM, WPARAM},
46                    UI::{
47                        Input::KeyboardAndMouse::VK_RETURN,
48                        WindowsAndMessaging::{PostMessageW, WM_KEYDOWN},
49                    },
50                };
51
52                unsafe {
53                    // Select submenu item:
54                    _ = PostMessageW(
55                        context_menu,
56                        0x1e5,
57                        WPARAM(usize::try_from(index).unwrap()),
58                        LPARAM(0),
59                    );
60                    // Activate it:
61                    _ = PostMessageW(
62                        context_menu,
63                        WM_KEYDOWN,
64                        WPARAM(usize::from(VK_RETURN.0)),
65                        LPARAM(0),
66                    );
67                }
68            }
69            SubMenu::AccessKey(key) => {
70                use windows::Win32::{
71                    Foundation::{LPARAM, WPARAM},
72                    UI::WindowsAndMessaging::{PostMessageW, WM_KEYDOWN},
73                };
74                unsafe {
75                    _ = PostMessageW(
76                        context_menu,
77                        WM_KEYDOWN,
78                        WPARAM(usize::from(key)),
79                        LPARAM(0),
80                    );
81                }
82            }
83        }
84    }
85}
86
87/// Listens for context menu events to be able to then focus on submenu items
88/// and expand them.
89#[derive(Default, nwd::NwgPartial)]
90pub struct OpenSubmenuPlugin {
91    submenus: RefCell<VecDeque<SubMenu>>,
92    queued_at: Cell<Option<Instant>>,
93}
94impl OpenSubmenuPlugin {
95    /// Ignore queued operations if the context menu isn't opened after this
96    /// much time.
97    const MAX_DELAY: Duration = Duration::from_millis(4000);
98
99    pub fn queue_open_of(&self, submenu: impl IntoIterator<Item = SubMenu>) {
100        let items = submenu.into_iter().collect::<Vec<_>>();
101        let mut guard = self.submenus.borrow_mut();
102        guard.clear();
103        guard.extend(items);
104        self.queued_at.set(Some(Instant::now()));
105    }
106}
107impl DynamicUiHooks<SystemTray> for OpenSubmenuPlugin {
108    fn before_partial_build(
109        &mut self,
110        tray_ui: &Rc<SystemTray>,
111        _should_build: &mut bool,
112    ) -> Option<(nwg::ControlHandle, TypeId)> {
113        Some((tray_ui.root().tray_menu.handle, TypeId::of::<TrayRoot>()))
114    }
115    fn after_process_events(
116        &self,
117        _dynamic_ui: &Rc<SystemTray>,
118        evt: nwg::Event,
119        _evt_data: &nwg::EventData,
120        _handle: nwg::ControlHandle,
121        _window: nwg::ControlHandle,
122    ) {
123        if let nwg::Event::OnMenuOpen = evt {
124            let Some(queue_time) = self.queued_at.get() else {
125                // Nothing queued.
126                return;
127            };
128            if queue_time.elapsed() > Self::MAX_DELAY {
129                // Action was queued long ago and no longer seems relevant.
130                self.submenus.borrow_mut().clear();
131                self.queued_at.set(None);
132                return;
133            }
134            let Some(next) = self.submenus.borrow_mut().pop_front() else {
135                self.queued_at.set(None);
136                return;
137            };
138            next.open();
139        }
140    }
141}
142impl TrayPlugin for OpenSubmenuPlugin {}
143
144/// A submenu item (with an extra separator) that can be used as the parent of
145/// the quick switch menu items.
146#[derive(Default, nwd::NwgPartial)]
147pub struct QuickSwitchTopMenu {
148    #[nwg_control(text: "&Quick Switch")]
149    tray_quick_menu: nwg::Menu,
150
151    #[nwg_control()]
152    tray_sep: nwg::MenuSeparator,
153
154    is_built: bool,
155}
156impl QuickSwitchTopMenu {
157    /// Handle to the root quick switch submenu if it is enabled and built.
158    pub fn menu_handle(&self) -> Option<nwg::ControlHandle> {
159        Some(self.tray_quick_menu.handle).filter(|_| self.is_built)
160    }
161}
162impl DynamicUiHooks<SystemTray> for QuickSwitchTopMenu {
163    fn before_partial_build(
164        &mut self,
165        tray_ui: &Rc<SystemTray>,
166        should_build: &mut bool,
167    ) -> Option<(nwg::ControlHandle, TypeId)> {
168        let should_enable = tray_ui.settings().get().quick_switch_menu == QuickSwitchMenu::SubMenu;
169        if !should_enable {
170            *should_build = false;
171            return None;
172        }
173        Some((tray_ui.root().tray_menu.handle, TypeId::of::<TrayRoot>()))
174    }
175    fn after_partial_build(&mut self, _dynamic_ui: &Rc<SystemTray>) {
176        self.is_built = true;
177    }
178    fn need_rebuild(&self, tray_ui: &Rc<SystemTray>) -> bool {
179        let should_enable = tray_ui.settings().get().quick_switch_menu == QuickSwitchMenu::SubMenu;
180        should_enable != self.is_built
181    }
182    fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
183        menu_remove(&self.tray_quick_menu);
184        *self = Default::default();
185    }
186}
187impl TrayPlugin for QuickSwitchTopMenu {
188    fn on_current_desktop_changed(&self, _tray_ui: &Rc<SystemTray>, current_desktop_index: u32) {
189        if !self.is_built {
190            return;
191        }
192        crate::nwg_ext::menu_set_text(
193            self.tray_quick_menu.handle,
194            &format!("&Quick Switch from Desktop {}", current_desktop_index + 1),
195        );
196    }
197}
198
199#[derive(Default, nwd::NwgPartial)]
200pub struct TopMenuItems {
201    tray_ui: SystemTrayRef,
202
203    #[nwg_control(text: "&Close Current Desktop")]
204    #[nwg_events(OnMenuItemSelected: [Self::close_current_desktop])]
205    tray_close_desktop: nwg::MenuItem,
206
207    #[nwg_control(text: "&New Desktop")]
208    #[nwg_events(OnMenuItemSelected: [Self::create_desktop])]
209    tray_create_desktop: nwg::MenuItem,
210
211    #[nwg_control()]
212    tray_sep1: nwg::MenuSeparator,
213
214    #[nwg_control(text: "&Smooth Desktop Switch")]
215    #[nwg_events(OnMenuItemSelected: [Self::toggle_smooth_switch])]
216    pub tray_smooth_switch: nwg::MenuItem,
217
218    #[nwg_control(text: "More &Options")]
219    tray_settings_menu: nwg::Menu,
220
221    #[cfg(feature = "admin_startup")]
222    #[nwg_control(text: "&Request Admin at Startup", parent: tray_settings_menu)]
223    #[nwg_events(OnMenuItemSelected: [Self::toggle_request_admin_at_startup])]
224    tray_request_admin_at_startup: nwg::MenuItem,
225
226    #[nwg_control(text: "Tray &Icon", parent: tray_settings_menu)]
227    tray_icon_menu: nwg::Menu,
228
229    /// One menu item per icon type.
230    tray_icon_types: Vec<nwg::MenuItem>,
231
232    #[nwg_control(text: "&Quick Switch Menu", parent: tray_settings_menu)]
233    tray_quick_switch_menu: nwg::Menu,
234
235    /// One menu item per quick switch option.
236    tray_quick_switch_items: Vec<nwg::MenuItem>,
237
238    #[nwg_control(text: "Auto &Start", parent: tray_settings_menu)]
239    tray_auto_start_menu: nwg::Menu,
240
241    /// One menu item per auto start option.
242    tray_auto_start_items: Vec<nwg::MenuItem>,
243
244    #[nwg_control()]
245    tray_sep2: nwg::MenuSeparator,
246}
247impl DynamicUiHooks<SystemTray> for TopMenuItems {
248    fn before_partial_build(
249        &mut self,
250        tray_ui: &Rc<SystemTray>,
251        _should_build: &mut bool,
252    ) -> Option<(nwg::ControlHandle, TypeId)> {
253        Some((tray_ui.root().tray_menu.handle, TypeId::of::<TrayRoot>()))
254    }
255    fn after_partial_build(&mut self, tray_ui: &Rc<SystemTray>) {
256        self.tray_ui.set(tray_ui);
257        let settings = tray_ui.settings().get();
258
259        #[cfg(feature = "admin_startup")]
260        {
261            self.tray_request_admin_at_startup
262                .set_checked(settings.request_admin_at_startup);
263            self.update_label_for_request_admin_at_startup();
264        }
265
266        self.tray_smooth_switch
267            .set_checked(settings.smooth_switch_desktops);
268        self.update_label_for_smooth_switch();
269
270        {
271            let menu_items = &mut self.tray_icon_types;
272            menu_items.clear();
273
274            for tray_icon in TrayIconType::ALL {
275                let mut item = Default::default();
276                let res = nwg::MenuItem::builder()
277                    .text(&format!("{tray_icon:?}"))
278                    .parent(self.tray_icon_menu.handle)
279                    .build(&mut item);
280                if let Err(e) = res {
281                    tracing::error!(
282                        "Failed to build menu item for tray icon type {tray_icon:?}: {e}"
283                    );
284                }
285                menu_items.push(item);
286            }
287        }
288        self.check_selected_tray_icon(settings.tray_icon_type);
289
290        {
291            let menu_items = &mut self.tray_quick_switch_items;
292            menu_items.clear();
293
294            for option in QuickSwitchMenu::ALL {
295                let mut item = Default::default();
296                let res = nwg::MenuItem::builder()
297                    .text(&format!("{option:?}"))
298                    .parent(self.tray_quick_switch_menu.handle)
299                    .build(&mut item);
300                if let Err(e) = res {
301                    tracing::error!(
302                        "Failed to build menu item for quick switch option \"{option:?}\": {e}"
303                    );
304                }
305                menu_items.push(item);
306            }
307        }
308        self.check_selected_quick_switch(settings.quick_switch_menu);
309
310        {
311            let menu_items = &mut self.tray_auto_start_items;
312            menu_items.clear();
313
314            for option in AutoStart::ALL {
315                let mut item = Default::default();
316                let res = nwg::MenuItem::builder()
317                    .text(&format!("{option:?}"))
318                    .parent(self.tray_auto_start_menu.handle)
319                    .build(&mut item);
320                if let Err(e) = res {
321                    tracing::error!(
322                        "Failed to build menu item for auto start option \"{option:?}\": {e}"
323                    );
324                }
325                menu_items.push(item);
326            }
327        }
328        self.check_selected_auto_start(settings.auto_start);
329    }
330    fn before_rebuild(&mut self, tray_ui: &Rc<SystemTray>) {
331        *self = Default::default();
332        self.tray_ui.set(tray_ui);
333    }
334    fn after_process_events(
335        &self,
336        dynamic_ui: &Rc<SystemTray>,
337        evt: nwg::Event,
338        _evt_data: &nwg::EventData,
339        handle: nwg::ControlHandle,
340        _window: nwg::ControlHandle,
341    ) {
342        if let nwg::Event::OnMenuItemSelected = evt {
343            let new_icon = self
344                .tray_icon_types
345                .iter()
346                .zip(TrayIconType::ALL)
347                .find(|(item, _)| item.handle == handle)
348                .map(|(_, icon)| *icon);
349
350            if let Some(new_icon) = new_icon {
351                dynamic_ui.settings().update(|prev| UiSettings {
352                    tray_icon_type: new_icon,
353                    ..prev.clone()
354                });
355            }
356
357            let wanted_quick_switch = self
358                .tray_quick_switch_items
359                .iter()
360                .zip(QuickSwitchMenu::ALL)
361                .find(|(item, _)| item.handle == handle)
362                .map(|(_, option)| *option);
363
364            if let Some(wanted_quick_switch) = wanted_quick_switch {
365                dynamic_ui.settings().update(|prev| UiSettings {
366                    quick_switch_menu: wanted_quick_switch,
367                    ..prev.clone()
368                });
369            }
370
371            let auto_start = self
372                .tray_auto_start_items
373                .iter()
374                .zip(AutoStart::ALL)
375                .find(|(item, _)| item.handle == handle)
376                .map(|(_, option)| *option);
377
378            if let Some(auto_start) = auto_start {
379                dynamic_ui.settings().update(|prev| UiSettings {
380                    auto_start,
381                    ..prev.clone()
382                });
383            }
384        }
385    }
386}
387impl TrayPlugin for TopMenuItems {
388    fn on_settings_changed(
389        &self,
390        _tray_ui: &Rc<SystemTray>,
391        prev: &Arc<UiSettings>,
392        settings: &Arc<UiSettings>,
393    ) {
394        #[cfg(feature = "admin_startup")]
395        {
396            if self.tray_request_admin_at_startup.checked() != settings.request_admin_at_startup {
397                self.tray_request_admin_at_startup
398                    .set_checked(settings.request_admin_at_startup);
399                self.update_label_for_request_admin_at_startup();
400            }
401        }
402
403        if self.tray_smooth_switch.checked() != settings.smooth_switch_desktops {
404            self.tray_smooth_switch
405                .set_checked(settings.smooth_switch_desktops);
406            self.update_label_for_smooth_switch();
407        }
408
409        if prev.tray_icon_type != settings.tray_icon_type {
410            self.check_selected_tray_icon(settings.tray_icon_type);
411        }
412        if prev.quick_switch_menu != settings.quick_switch_menu {
413            self.check_selected_quick_switch(settings.quick_switch_menu);
414        }
415        if prev.auto_start != settings.auto_start {
416            self.check_selected_auto_start(settings.auto_start);
417        }
418    }
419}
420/// Handle clicked menu items.
421impl TopMenuItems {
422    fn close_current_desktop(&self) {
423        let Some(tray_ui) = self.tray_ui.get() else {
424            return;
425        };
426        let result = vd::get_current_desktop().and_then(|current| {
427            let ix = current.get_index()?;
428            vd::remove_desktop(
429                current,
430                // Fallback to the left but if we are at the first then fallback
431                // to the right:
432                vd::Desktop::from(ix.checked_sub(1).unwrap_or(1)),
433            )?;
434            Ok(())
435        });
436        if let Err(e) = result {
437            tray_ui.show_notification(
438                "Virtual Desktop Manager Error",
439                &format!("Failed to create a new virtual desktop with: {e:?}"),
440            );
441        }
442    }
443    fn create_desktop(&self) {
444        let Some(tray_ui) = self.tray_ui.get() else {
445            return;
446        };
447        if let Err(e) = vd::create_desktop() {
448            tray_ui.show_notification(
449                "Virtual Desktop Manager Error",
450                &format!("Failed to create a new virtual desktop with: {e:?}"),
451            );
452        }
453    }
454    fn toggle_smooth_switch(&self) {
455        let Some(tray_ui) = self.tray_ui.get() else {
456            return;
457        };
458        let new_value = !self.tray_smooth_switch.checked();
459        self.tray_smooth_switch.set_checked(new_value);
460        tray_ui.settings().update(|prev| UiSettings {
461            smooth_switch_desktops: new_value,
462            ..prev.clone()
463        });
464        self.update_label_for_smooth_switch();
465        tray_ui.show_menu(MenuPosition::AtPrevious);
466    }
467    #[cfg(feature = "admin_startup")]
468    fn toggle_request_admin_at_startup(&self) {
469        let Some(tray_ui) = self.tray_ui.get() else {
470            return;
471        };
472        let new_value = !self.tray_request_admin_at_startup.checked();
473        self.tray_request_admin_at_startup.set_checked(new_value);
474        tray_ui.settings().update(|prev| UiSettings {
475            request_admin_at_startup: new_value,
476            ..prev.clone()
477        });
478        self.update_label_for_request_admin_at_startup();
479        if let Some(plugin) = tray_ui.get_dynamic_ui().get_ui::<OpenSubmenuPlugin>() {
480            plugin.queue_open_of([SubMenu::Handle(self.tray_settings_menu.handle)]);
481        };
482        tray_ui.show_menu(MenuPosition::AtPrevious);
483    }
484}
485/// Helper methods.
486impl TopMenuItems {
487    #[cfg(feature = "admin_startup")]
488    fn update_label_for_request_admin_at_startup(&self) {
489        let checked = self.tray_request_admin_at_startup.checked();
490        crate::nwg_ext::menu_set_text(
491            self.tray_request_admin_at_startup.handle,
492            &format!(
493                "Request Admin at Startup ({})",
494                if checked { "On" } else { "Off" }
495            ),
496        );
497    }
498    fn update_label_for_smooth_switch(&self) {
499        let checked = self.tray_smooth_switch.checked();
500        crate::nwg_ext::menu_set_text(
501            self.tray_smooth_switch.handle,
502            &format!(
503                "&Smooth Desktop Switch ({})",
504                if checked { "On" } else { "Off" }
505            ),
506        );
507    }
508    fn check_selected_tray_icon(&self, selected: TrayIconType) {
509        let items = &self.tray_icon_types;
510        for (item, icon) in items.iter().zip(TrayIconType::ALL) {
511            let should_check = *icon == selected;
512            if should_check != item.checked() {
513                // This re-renders the item to ensure it gets updated if the context menu is open
514                item.set_enabled(true);
515                // Do this after `set_enabled` since that resets the checked status.
516                item.set_checked(should_check);
517            }
518        }
519    }
520    fn check_selected_quick_switch(&self, selected: QuickSwitchMenu) {
521        let items = &self.tray_quick_switch_items;
522        for (item, option) in items.iter().zip(QuickSwitchMenu::ALL) {
523            let should_check = *option == selected;
524            if should_check != item.checked() {
525                // This re-renders the item to ensure it gets updated if the context menu is open
526                item.set_enabled(true);
527                // Do this after `set_enabled` since that resets the checked status.
528                item.set_checked(should_check);
529            }
530        }
531    }
532    fn check_selected_auto_start(&self, selected: AutoStart) {
533        let items = &self.tray_auto_start_items;
534        for (item, option) in items.iter().zip(AutoStart::ALL) {
535            let should_check = *option == selected;
536            if should_check != item.checked() {
537                // This re-renders the item to ensure it gets updated if the context menu is open
538                item.set_enabled(true);
539                // Do this after `set_enabled` since that resets the checked status.
540                item.set_checked(should_check);
541            }
542        }
543    }
544}
545
546/// Context menu items to switch to another virtual desktop. Not nested under a
547/// submenu but rather all flat under the root menu. These are also "checked"
548/// when you are currently on that desktop.
549#[derive(Default)]
550pub struct FlatSwitchMenu {
551    tray_ui: SystemTrayRef,
552
553    /// Update right before UI build, so we can use this to track if we need to
554    /// rebuild.
555    desktop_count: u32,
556
557    /// One menu item per open virtual desktop.
558    tray_virtual_desktops: Vec<nwg::MenuItem>,
559}
560impl FlatSwitchMenu {
561    fn check_current_desktop(&self, current_desktop_index: u32) {
562        let desktops = self.tray_virtual_desktops.as_slice();
563        for (i, desktop) in desktops.iter().rev().enumerate() {
564            let is_current = i == current_desktop_index as usize;
565            let was_checked = desktop.checked();
566            if is_current != was_checked {
567                // This re-renders the item to ensure it gets updated if the context menu is open
568                desktop.set_enabled(true);
569                // Do this after `set_enabled` since it resets the checked status.
570                desktop.set_checked(is_current);
571            }
572        }
573    }
574}
575impl nwg::PartialUi for FlatSwitchMenu {
576    fn build_partial<W: Into<nwg::ControlHandle>>(
577        data: &mut Self,
578        parent: Option<W>,
579    ) -> Result<(), nwg::NwgError> {
580        let parent = parent.map(Into::into).ok_or_else(|| {
581            nwg::NwgError::MenuCreationError("No parent defined for FlatSwitchMenu".to_string())
582        })?;
583        {
584            let tray_desktops = &mut data.tray_virtual_desktops;
585            tray_desktops.clear();
586
587            for i in (1..=data.desktop_count.min(15)).rev() {
588                let mut item = Default::default();
589                nwg::MenuItem::builder()
590                    .text(&format!(
591                        "Virtual desktop {}{i}",
592                        if i < 10 { "&" } else { "" }
593                    ))
594                    .parent(parent)
595                    .build(&mut item)
596                    .map_err(|e| {
597                        nwg::NwgError::MenuCreationError(format!(
598                            "Failed to build menu item for FlatSwitchMenu: {e}"
599                        ))
600                    })?;
601                tray_desktops.push(item);
602            }
603        }
604
605        // After we rebuilt the context menu, we need to mark the currently
606        // active virtual desktop:
607        if let Some(tray_ui) = data.tray_ui.get() {
608            data.check_current_desktop(tray_ui.desktop_index.get());
609        }
610
611        Ok(())
612    }
613    fn process_event(
614        &self,
615        evt: nwg::Event,
616        _evt_data: &nwg::EventData,
617        handle: nwg::ControlHandle,
618    ) {
619        if let nwg::Event::OnMenuItemSelected = evt {
620            let desktop_ix = self
621                .tray_virtual_desktops
622                .iter()
623                .rev()
624                .position(|d| d.handle == handle);
625            if let Some(clicked_desktop_ix) = desktop_ix {
626                if let Some(tray_ui) = self.tray_ui.get() {
627                    tray_ui.switch_desktop(clicked_desktop_ix as u32);
628                }
629            }
630        }
631    }
632}
633impl DynamicUiHooks<SystemTray> for FlatSwitchMenu {
634    fn before_partial_build(
635        &mut self,
636        tray_ui: &Rc<SystemTray>,
637        should_build: &mut bool,
638    ) -> Option<(nwg::ControlHandle, TypeId)> {
639        if tray_ui.settings().get().quick_switch_menu == QuickSwitchMenu::TopMenu {
640            *should_build = false;
641            return None;
642        }
643        self.desktop_count = tray_ui.desktop_count.get();
644        self.tray_ui.set(tray_ui);
645        Some((tray_ui.root().tray_menu.handle, TypeId::of::<TrayRoot>()))
646    }
647    fn need_rebuild(&self, tray_ui: &Rc<SystemTray>) -> bool {
648        if tray_ui.settings().get().quick_switch_menu == QuickSwitchMenu::TopMenu {
649            self.desktop_count != 0 // Want 0 flat switch items
650        } else {
651            self.desktop_count != tray_ui.desktop_count.get()
652        }
653    }
654}
655impl TrayPlugin for FlatSwitchMenu {
656    fn on_current_desktop_changed(&self, _tray_ui: &Rc<SystemTray>, current_desktop_index: u32) {
657        self.check_current_desktop(current_desktop_index);
658    }
659}
660
661/// Listens for backspace key presses and sends escape key events when they
662/// occur. This allows backspace to be used to close submenus which works quite
663/// intuitively with the quick switch menu.
664#[derive(Default, nwd::NwgPartial)]
665pub struct BackspaceAsEscapeAlias {}
666impl DynamicUiHooks<SystemTray> for BackspaceAsEscapeAlias {
667    fn before_partial_build(
668        &mut self,
669        _dynamic_ui: &Rc<SystemTray>,
670        _should_build: &mut bool,
671    ) -> Option<(nwg::ControlHandle, TypeId)> {
672        None
673    }
674}
675impl TrayPlugin for BackspaceAsEscapeAlias {
676    fn on_menu_key_press(
677        &self,
678        _tray_ui: &Rc<SystemTray>,
679        key_code: u32,
680        _menu_handle: isize,
681    ) -> Option<MenuKeyPressEffect> {
682        if key_code != 8 {
683            // Not backspace key
684            return None;
685        }
686        'simulate_escape: {
687            use windows::Win32::{
688                Foundation::{LPARAM, WPARAM},
689                UI::{
690                    Input::KeyboardAndMouse::VK_ESCAPE,
691                    WindowsAndMessaging::{SendMessageW, WM_KEYDOWN},
692                },
693            };
694
695            let Some(context_menu_window) = crate::nwg_ext::find_context_menu_window() else {
696                tracing::warn!("Unable to find context menu window");
697                break 'simulate_escape;
698            };
699            unsafe {
700                SendMessageW(
701                    context_menu_window,
702                    WM_KEYDOWN,
703                    WPARAM(usize::from(VK_ESCAPE.0)),
704                    LPARAM(0),
705                );
706            }
707        }
708        Some(MenuKeyPressEffect::SelectIndex(0))
709    }
710}
711
712/// Create quick switch menu that makes use of keyboard access keys to allow for
713/// fast navigation (Note: you can use Win+B to select the toolbar and then the
714/// Enter key to open the context menu, after that you can press `Q` to open the
715/// quick switch menu):
716#[derive(Default)]
717pub struct QuickSwitchMenuUiAdapter {
718    tray_ui: SystemTrayRef,
719
720    /// Update right before UI build, so we can use this to track if we need to
721    /// rebuild.
722    desktop_count: u32,
723
724    /// Extra separators when inside top menu.
725    extra_separators: Option<(nwg::MenuSeparator, nwg::MenuSeparator)>,
726
727    parent: nwg::ControlHandle,
728
729    tray_quick_menu_state: crate::quick_switch::QuickSwitchMenu,
730}
731impl nwg::PartialUi for QuickSwitchMenuUiAdapter {
732    fn build_partial<W: Into<nwg::ControlHandle>>(
733        data: &mut Self,
734        parent: Option<W>,
735    ) -> Result<(), nwg::NwgError> {
736        let parent = parent.map(Into::into).ok_or_else(|| {
737            nwg::NwgError::MenuCreationError("No parent defined for quick switch menu".to_string())
738        })?;
739        data.parent = parent;
740        if let Some((first, _)) = &mut data.extra_separators {
741            MenuSeparator::builder().parent(parent).build(first)?;
742        }
743
744        let quick = &mut data.tray_quick_menu_state;
745        quick.clear();
746        quick.create_quick_switch_menu(parent, data.desktop_count + 1);
747
748        if let Some((_, last)) = &mut data.extra_separators {
749            MenuSeparator::builder().parent(parent).build(last)?;
750        }
751        Ok(())
752    }
753    fn process_event(
754        &self,
755        evt: nwg::Event,
756        _evt_data: &nwg::EventData,
757        handle: nwg::ControlHandle,
758    ) {
759        if let nwg::Event::OnMenuItemSelected = evt {
760            let desktop_ix = self.tray_quick_menu_state.get_clicked_desktop_index(handle);
761            if let Some(clicked_desktop_ix) = desktop_ix {
762                if let Some(tray_ui) = self.tray_ui.get() {
763                    tray_ui.switch_desktop(clicked_desktop_ix as u32);
764                }
765            }
766        }
767    }
768}
769impl DynamicUiHooks<SystemTray> for QuickSwitchMenuUiAdapter {
770    fn before_partial_build(
771        &mut self,
772        tray_ui: &Rc<SystemTray>,
773        should_build: &mut bool,
774    ) -> Option<(nwg::ControlHandle, TypeId)> {
775        let settings = tray_ui.settings().get();
776        self.tray_quick_menu_state.shortcuts =
777            BTreeMap::clone(&settings.quick_switch_menu_shortcuts);
778        self.tray_quick_menu_state.shortcuts_only_in_root =
779            settings.quick_switch_menu_shortcuts_only_in_root;
780        if settings.quick_switch_menu == QuickSwitchMenu::Disabled {
781            *should_build = false;
782            return None;
783        }
784        self.tray_ui.set(tray_ui);
785        let (parent_handle, parent_id) = if let Some(menu) = tray_ui
786            .dynamic_ui
787            .get_ui::<QuickSwitchTopMenu>()
788            .filter(|top| top.is_built)
789        {
790            (
791                menu.tray_quick_menu.handle,
792                TypeId::of::<QuickSwitchTopMenu>(),
793            )
794        } else {
795            tracing::info!(
796                "No QuickSwitchTopMenu so quick switch menu will be inlined in the root context menu"
797            );
798            self.extra_separators = Some(Default::default());
799            (tray_ui.root().tray_menu.handle, TypeId::of::<TrayRoot>())
800        };
801        self.desktop_count = tray_ui.desktop_count.get();
802        Some((parent_handle, parent_id))
803    }
804    fn need_rebuild(&self, tray_ui: &Rc<SystemTray>) -> bool {
805        let settings = tray_ui.settings().get();
806        let has_moved_menu = if settings.quick_switch_menu == QuickSwitchMenu::Disabled {
807            self.desktop_count != 0
808        } else {
809            self.desktop_count != tray_ui.desktop_count.get()
810        };
811        let has_changed_shortcuts = settings.quick_switch_menu_shortcuts_only_in_root
812            != self.tray_quick_menu_state.shortcuts_only_in_root
813            || *settings.quick_switch_menu_shortcuts != self.tray_quick_menu_state.shortcuts;
814        has_moved_menu || has_changed_shortcuts
815    }
816    fn before_rebuild(&mut self, _tray_ui: &Rc<SystemTray>) {
817        // Reuse quick menu internal capacity:
818        let quick = std::mem::take(&mut self.tray_quick_menu_state);
819        *self = Default::default();
820        self.tray_quick_menu_state = quick;
821        self.tray_quick_menu_state.clear();
822    }
823}
824impl TrayPlugin for QuickSwitchMenuUiAdapter {
825    fn on_menu_key_press(
826        &self,
827        tray_ui: &Rc<SystemTray>,
828        key_code: u32,
829        menu_handle: isize,
830    ) -> Option<MenuKeyPressEffect> {
831        let key = char::from_u32(key_code)?;
832        if key == 'q' || key == 'Q' {
833            let parent = match self.parent {
834                nwg::ControlHandle::Menu(_, h) => h as isize,
835                nwg::ControlHandle::PopMenu(_, h) => h as isize,
836                _ => {
837                    tracing::error!("Parent to quick switch menu wasn't a menu");
838                    return None;
839                }
840            };
841            if parent != menu_handle {
842                return None; // Not inside same menu as quick switch items
843            }
844            let item = self
845                .tray_quick_menu_state
846                .first_item_in_submenu(menu_handle)?;
847            return Some(MenuKeyPressEffect::Select(item));
848        }
849        if key != ' ' {
850            return None;
851        }
852        let Some(wanted_ix) = self
853            .tray_quick_menu_state
854            .get_desktop_index_so_far(menu_handle)
855        else {
856            tracing::debug!("Could not find quick switch submenu when pressing space");
857            return None;
858        };
859        tracing::info!(
860            "Pressed space while inside a quick switch context submenu that \
861            would have been opened by pressing the access keys corresponding \
862            to the desktop with the one-based index {}",
863            wanted_ix + 1
864        );
865        tray_ui.switch_desktop(wanted_ix as u32);
866        Some(MenuKeyPressEffect::Close)
867    }
868}
869
870#[derive(Default, nwd::NwgPartial)]
871pub struct BottomMenuItems {
872    tray_ui: SystemTrayRef,
873
874    #[nwg_control]
875    tray_sep1: nwg::MenuSeparator,
876
877    #[nwg_control(text: "Stop &Flashing Windows")]
878    #[nwg_events(OnMenuItemSelected: [Self::stop_flashing_windows])]
879    tray_stop_flashing: nwg::MenuItem,
880
881    #[nwg_control(text: "Configure Filters")]
882    #[nwg_events(OnMenuItemSelected: [Self::open_filter_config])]
883    tray_configure_filters: nwg::MenuItem,
884
885    #[nwg_control(text: "Apply Filters")]
886    #[nwg_events(OnMenuItemSelected: [Self::apply_filters])]
887    tray_apply_filters: nwg::MenuItem,
888
889    #[nwg_control]
890    tray_sep2: nwg::MenuSeparator,
891
892    #[nwg_control(text: "Exit")]
893    #[nwg_events(OnMenuItemSelected: [Self::exit])]
894    tray_exit: nwg::MenuItem,
895}
896/// Handle menu clicks.
897impl BottomMenuItems {
898    forward_to_dynamic_ui!(tray_ui => apply_filters, stop_flashing_windows, exit);
899
900    fn open_filter_config(&self) {
901        let Some(tray_ui) = self.tray_ui.get() else {
902            return;
903        };
904        tray_ui.configure_filters(true);
905    }
906}
907impl DynamicUiHooks<SystemTray> for BottomMenuItems {
908    fn before_partial_build(
909        &mut self,
910        tray_ui: &Rc<SystemTray>,
911        _should_build: &mut bool,
912    ) -> Option<(nwg::ControlHandle, TypeId)> {
913        self.tray_ui.set(tray_ui);
914        Some((tray_ui.root().tray_menu.handle, TypeId::of::<TrayRoot>()))
915    }
916}
917impl TrayPlugin for BottomMenuItems {}