virtual_desktop_manager/
tray.rs

1//! Defines the system tray and global program state.
2
3use std::{
4    any::TypeId,
5    cell::{Cell, Ref, RefCell},
6    rc::Rc,
7    sync::Arc,
8    time::{Duration, Instant},
9};
10
11use windows::Win32::Foundation::HWND;
12
13use crate::{
14    config_window::ConfigWindow,
15    dynamic_gui::{DynamicUi, DynamicUiHooks, DynamicUiOwner, DynamicUiRef, DynamicUiWrapper},
16    invisible_window::SmoothDesktopSwitcher,
17    nwg_ext::{
18        menu_index_in_parent, menu_item_index_in_parent, tray_get_rect, tray_set_version_4,
19        windows_msg_for_explorer_restart, FastTimerControl, TrayWindow,
20    },
21    settings::{TrayClickAction, UiSettings},
22    vd,
23};
24
25/// Basic state used by the program.
26#[derive(Default, nwd::NwgPartial)]
27pub struct TrayRoot {
28    tray_ui: SystemTrayRef,
29
30    no_parent: crate::nwg_ext::ParentCapture,
31
32    #[nwg_control(parent: no_parent)]
33    pub window: TrayWindow,
34
35    /// The initial icon for the tray.
36    #[nwg_resource(source_embed: Some(&nwg::EmbedResource::load(None).unwrap()), source_embed_id: 1)]
37    //#[nwg_resource(source_bin: Some(crate::tray_icons::ICON_EMPTY))]
38    pub icon: nwg::Icon,
39
40    #[nwg_control(parent: window, icon: Some(&data.icon), tip: Some("Virtual Desktop Manager"))]
41    #[nwg_events(
42        MousePressLeftUp: [Self::notify_tray_left_click],
43        // Handled manually in process_raw_event:
44        // OnContextMenu: [Self::show_menu]
45    )]
46    pub tray: nwg::TrayNotification,
47
48    last_tray_key_event: Cell<Option<Instant>>,
49
50    last_left_click: Cell<Option<Instant>>,
51
52    #[nwg_control(parent: window, popup: true)]
53    pub tray_menu: nwg::Menu,
54
55    selected_tray_menu_item: Cell<Option<nwg::ControlHandle>>,
56
57    /// The location where we last showed the context menu.
58    last_menu_pos: Cell<Option<(i32, i32)>>,
59
60    reshow_tray_menu: Cell<Option<(Instant, MenuPosition)>>,
61
62    #[nwg_control(parent: window)]
63    #[nwg_events(OnNotice: [Self::notify_reshow_tray_menu_delayed])]
64    reshow_tray_menu_delay: FastTimerControl,
65
66    /// If the app is auto started with Windows then the taskbar might not exist
67    /// when the program is started and if so we need to re-register our tray
68    /// icon.
69    #[nwg_control(parent: window)]
70    #[nwg_events(OnNotice: [Self::notify_startup_rebuild])]
71    rebuild_at_startup: FastTimerControl,
72    /// The program was started at approximately this time.
73    first_created_at: Option<Instant>,
74
75    /// Initial virtual desktop count might be incorrect if program is started with Windows.
76    #[nwg_control(parent: window)]
77    #[nwg_events(OnNotice: [Self::notify_check_desktop_count])]
78    recheck_virtual_desktop_init: FastTimerControl,
79
80    need_rebuild: Cell<bool>,
81}
82impl TrayRoot {
83    pub fn notify_that_tray_icon_exists(&self) {
84        self.rebuild_at_startup.cancel_last();
85    }
86    fn notify_tray_left_click(&self) {
87        let Some(tray_ui) = self.tray_ui.get() else {
88            return;
89        };
90
91        let now = Instant::now();
92        if let Some(last_left_click) = self.last_left_click.replace(Some(now)) {
93            if now.duration_since(last_left_click) < Duration::from_millis(300) {
94                // Double click should have the same outcome as single click so
95                // we ignore the second click.
96                tracing::debug!("Ignored double left click event on tray icon");
97                return;
98            }
99        }
100
101        tray_ui.notify_tray_left_click();
102    }
103
104    fn notify_startup_rebuild(&self) {
105        let Some(tray_ui) = self.tray_ui.get() else {
106            return;
107        };
108        tracing::info!(
109            "Rebuilding tray icon incase the taskbar didn't exist when the program was started"
110        );
111        tray_ui.notify_explorer_restart();
112    }
113
114    fn notify_check_desktop_count(&self) {
115        // TODO: move this code to tray_plugins::desktop_events
116        let Some(tray_ui) = self.tray_ui.get() else {
117            return;
118        };
119        if tray_ui.desktop_count.get() <= 1 {
120            // Might have failed to get desktop count at startup, so try again
121            if matches!(vd::get_desktop_count(), Err(_) | Ok(0 | 1))
122                // Recheck count for about 2 minutes after startup in case
123                // virtual desktops haven't been initialized yet:
124                && self.first_created_at.is_some_and(
125                    |created_at| Instant::now()
126                        .saturating_duration_since(created_at)
127                        > Duration::from_secs(120)
128                    )
129            {
130                self.recheck_virtual_desktop_init
131                    .notify_after(Duration::from_millis(1000));
132            } else {
133                tray_ui.update_desktop_info();
134                tray_ui.dynamic_ui.for_each_ui(|plugin| {
135                    plugin.on_desktop_count_changed(&tray_ui, tray_ui.desktop_count.get())
136                });
137            }
138        }
139    }
140
141    fn notify_reshow_tray_menu_delayed(&self) {
142        let Some(tray_ui) = self.tray_ui.get() else {
143            return;
144        };
145        let Some((queued_at, position)) = self.reshow_tray_menu.take() else {
146            return;
147        };
148        tracing::debug!(?position, time_since_requested = ?queued_at.elapsed(), "Re-show context menu");
149        tray_ui.show_menu(position);
150    }
151
152    #[allow(dead_code)]
153    pub fn get_selected_tray_menu_item(&self) -> Option<nwg::ControlHandle> {
154        self.selected_tray_menu_item.get()
155    }
156
157    pub fn update_tray_icon(&self, tray_ui: &Rc<SystemTray>, new_ix: u32) {
158        use crate::{settings::TrayIconType, tray_icons::IconType};
159
160        let icon_type = tray_ui.settings().get().tray_icon_type;
161        let icon_generator = match icon_type {
162            TrayIconType::WithBackground => IconType::WithBackground {
163                allow_hardcoded: true,
164                light_theme: tray_ui.has_light_taskbar(),
165            },
166            TrayIconType::WithBackgroundNoHardcoded => IconType::WithBackground {
167                allow_hardcoded: false,
168                light_theme: tray_ui.has_light_taskbar(),
169            },
170            TrayIconType::NoBackground => IconType::NoBackground {
171                light_theme: tray_ui.has_light_taskbar(),
172            },
173            TrayIconType::NoBackground2 => IconType::NoBackgroundAlt,
174            TrayIconType::AppIcon => {
175                self.tray.set_icon(&self.icon);
176                return;
177            }
178        };
179        let icon_data = icon_generator.generate_icon(new_ix + 1);
180        if let Ok(icon) = nwg::Icon::from_bin(&icon_data) {
181            self.tray.set_icon(&icon);
182        }
183    }
184}
185impl DynamicUiHooks<SystemTray> for TrayRoot {
186    fn before_partial_build(
187        &mut self,
188        _dynamic_ui: &Rc<SystemTray>,
189        _should_build: &mut bool,
190    ) -> Option<(nwg::ControlHandle, TypeId)> {
191        None
192    }
193
194    fn after_partial_build(&mut self, tray_ui: &Rc<SystemTray>) {
195        tracing::debug!(
196            tray_window_handle = ?self.window.handle,
197            "Created new tray window"
198        );
199
200        self.tray_ui.set(tray_ui);
201        self.on_current_desktop_changed(tray_ui, tray_ui.desktop_index.get());
202
203        // Modern context menu handling:
204        //
205        // Note: the program need to be DPI aware (see program manifest) in
206        // order to get the right tray icon coordinates when opening context
207        // menu.
208        tray_set_version_4(&self.tray);
209
210        // Ensure this runs at least once, otherwise the message is never registered:
211        windows_msg_for_explorer_restart();
212
213        // Rebuild tray later since the windows taskbar might not exist right
214        // now (if Windows was just started):
215        let first_created_at = *self.first_created_at.get_or_insert_with(Instant::now);
216        let now = Instant::now();
217        let rebuild_after = [30, 60, 90];
218        for delay in rebuild_after {
219            let rebuild_at = first_created_at + Duration::from_secs(delay);
220            if rebuild_at > now {
221                self.rebuild_at_startup.notify_at(rebuild_at);
222                break;
223            }
224        }
225
226        if tray_ui.desktop_count.get() == 1 {
227            self.recheck_virtual_desktop_init
228                .notify_after(Duration::from_millis(10));
229        }
230    }
231
232    fn after_handles<'a>(
233        &'a self,
234        _dynamic_ui: &Rc<SystemTray>,
235        handles: &mut Vec<&'a nwg::ControlHandle>,
236    ) {
237        if handles.is_empty() {
238            handles.push(&self.window.handle)
239        }
240    }
241
242    fn after_process_events(
243        &self,
244        _dynamic_ui: &Rc<SystemTray>,
245        evt: nwg::Event,
246        _evt_data: &nwg::EventData,
247        handle: nwg::ControlHandle,
248        _window: nwg::ControlHandle,
249    ) {
250        match evt {
251            nwg::Event::OnMenuEnter | nwg::Event::OnMenuExit | nwg::Event::OnMenuItemSelected => {
252                self.selected_tray_menu_item.set(None)
253            }
254
255            // evt_data is None.
256            // handle is hovered item.
257            nwg::Event::OnMenuHover => self.selected_tray_menu_item.set(Some(handle)),
258
259            _ => {}
260        }
261    }
262    fn process_raw_event(
263        &self,
264        tray_ui: &Rc<SystemTray>,
265        _hwnd: isize,
266        msg: u32,
267        w: usize,
268        l: isize,
269        _window: nwg::ControlHandle,
270    ) -> Option<isize> {
271        use windows::Win32::UI::{
272            Shell::{NINF_KEY, NIN_SELECT},
273            WindowsAndMessaging::{
274                WM_CONTEXTMENU, WM_DPICHANGED, WM_ENTERIDLE, WM_EXITMENULOOP, WM_MBUTTONDOWN,
275                WM_MENUCHAR, WM_MOUSEFIRST, WM_RBUTTONUP, WM_THEMECHANGED, WM_USER,
276                WM_WININICHANGE,
277            },
278        };
279        /// [NIN_KEYSELECT missing](https://github.com/microsoft/win32metadata/issues/1765)
280        const NIN_KEYSELECT: u32 = NINF_KEY | NIN_SELECT;
281
282        // List of messages:
283        // https://wiki.winehq.org/List_Of_Windows_Messages
284        // https://stackoverflow.com/questions/8824255/getting-a-windows-message-name
285
286        /// From `nwg::win32::windows_helper`
287        const NWG_TRAY: u32 = WM_USER + 102;
288
289        // This gets tray events the same way as `nwg::win32::window::process_events`
290        if msg != NWG_TRAY {
291            if ![1124, 148, WM_ENTERIDLE].contains(&msg) {
292                #[cfg(all(feature = "logging", debug_assertions))]
293                tracing::trace!(
294                    msg,
295                    name = crate::wm_msg_to_string::wm_msg_to_string(msg),
296                    l = l,
297                    w = w,
298                    handle = _hwnd,
299                    "Non-tray event"
300                );
301            }
302
303            if msg == WM_EXITMENULOOP {
304                tray_ui.notify_tray_menu_closed();
305            } else if msg == WM_MENUCHAR {
306                // https://learn.microsoft.com/en-us/windows/win32/menurc/wm-menuchar
307                // wParam: low order is key, high order is menu type
308                // lParam: handle to active menu (not the selected/hovered item)
309                let key_code = w as u32 & 0xFFFF;
310                tracing::info!(
311                    key = ?char::from_u32(key_code),
312                    key_code = key_code,
313                    menu_handle = format!("{l:x}"),
314                    "Pressed key inside menu"
315                );
316                if let Some(effect) = tray_ui.notify_key_press_in_menu(key_code, l) {
317                    tracing::debug!(
318                        ?effect,
319                        "Choose manual effect in response to keyboard button press"
320                    );
321                    let (should_execute, item_index) = match effect {
322                        MenuKeyPressEffect::Ignore => return Some(0),
323                        MenuKeyPressEffect::Close => {
324                            // 1 in high-order word (above the first 16 bit):
325                            return Some(1 << 16);
326                        }
327                        MenuKeyPressEffect::Execute(handle)
328                        | MenuKeyPressEffect::Select(handle) => {
329                            let should_execute = matches!(effect, MenuKeyPressEffect::Execute(..));
330                            let item_index = match handle {
331                                nwg::ControlHandle::Menu(..) => menu_index_in_parent(handle),
332                                nwg::ControlHandle::MenuItem(..) => {
333                                    menu_item_index_in_parent(handle)
334                                }
335                                _ => {
336                                    tracing::error!(?handle, "Unsupported handle type");
337                                    return Some(0);
338                                }
339                            };
340                            let Some(item_index) = item_index else {
341                                tracing::error!(
342                                    ?handle,
343                                    "Failed to find index of sub menu in its parent"
344                                );
345                                return Some(0);
346                            };
347                            (should_execute, item_index as isize)
348                        }
349                        MenuKeyPressEffect::SelectIndex(index) => (false, index as isize),
350                    };
351
352                    if item_index >= (1 << 16) {
353                        tracing::error!(?item_index, "Menu item index is too large");
354                        return Some(0);
355                    }
356                    if should_execute {
357                        tracing::debug!(?effect, "Executing menu item at index {item_index}");
358                        // 2 in high-order word (above the first 16 bit):
359                        return Some(2 << 16 | item_index);
360                    } else {
361                        tracing::debug!(?effect, "Selecting menu item at index {item_index}");
362                        // 3 in high-order word (above the first 16 bit):
363                        return Some(3 << 16 | item_index);
364                    }
365                }
366            } else if msg == WM_THEMECHANGED || msg == WM_WININICHANGE {
367                tray_ui.notify_windows_mode_change();
368            } else if msg == windows_msg_for_explorer_restart() {
369                tray_ui.notify_explorer_restart();
370            } else if msg == WM_DPICHANGED {
371                // Seems this is needed to ensure we get the right coordinates for the tray icon
372                // https://stackoverflow.com/questions/41649303/difference-between-notifyicon-version-and-notifyicon-version-4-used-in-notifyico#comment116492307_54639792
373                tracing::info!("Rebuilding tray icon since DPI changed");
374                tray_ui.root().need_rebuild.set(true);
375            }
376            return None;
377        }
378        let msg = l as u32 & 0xffff;
379        // contains the icon ID:
380        let _other_l = l as u32 & (!0xffff);
381        let x = (w & 0xffff) as i16;
382        let y = ((w >> 16) & 0xffff) as i16;
383
384        if ![WM_MOUSEFIRST].contains(&msg) {
385            #[cfg(all(feature = "logging", debug_assertions))]
386            tracing::trace!(
387                msg,
388                name =? crate::wm_msg_to_string::wm_msg_to_string(msg),
389                w = w,
390                other_l = _other_l,
391                l_as_pos =? (x, y),
392                handle = _hwnd,
393                "Tray event"
394            );
395        }
396
397        match msg {
398            // Left click with tray version 4:
399            NIN_SELECT => {}
400            // Enter or spacebar when tray is selected:
401            NIN_KEYSELECT => {
402                // We receive this twice when enter is pressed but once when pressing space:
403                // https://github.com/openjdk/jdk/blob/72ca7bafcd49a98c1fe09da72e4e47683f052e9d/src/java.desktop/windows/native/libawt/windows/awt_TrayIcon.cpp#L449
404                let now = Instant::now();
405                if let Some(prev_time) = self.last_tray_key_event.replace(Some(now)) {
406                    let duration = now.duration_since(prev_time);
407                    if duration < Duration::from_millis(100) {
408                        // Likely double event
409                        tracing::debug!("Ignored double keypress event on tray icon");
410                        return None;
411                    }
412                }
413
414                tray_ui.notify_tray_left_click();
415            }
416            WM_MBUTTONDOWN => {
417                tray_ui.notify_tray_middle_click();
418            }
419            WM_RBUTTONUP => {
420                // Right mouse click on tray icon, after this we will receive a WM_CONTEXTMENU
421            }
422            // Only if using tray icon with version 4:
423            WM_CONTEXTMENU => {
424                self.notify_that_tray_icon_exists();
425                tray_ui.show_menu(MenuPosition::At(i32::from(x), i32::from(y)));
426            }
427            _ => {}
428        }
429
430        None
431    }
432
433    fn need_rebuild(&self, _dynamic_ui: &Rc<SystemTray>) -> bool {
434        self.need_rebuild.get()
435    }
436
437    fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
438        // Tray icon doesn't disappear when program quits, so we need to remove
439        // it manually:
440        self.tray.set_visibility(false);
441
442        *self = Self {
443            first_created_at: self.first_created_at,
444            ..Default::default()
445        };
446    }
447}
448impl TrayPlugin for TrayRoot {
449    fn on_windows_mode_changed(&self, tray_ui: &Rc<SystemTray>) {
450        self.update_tray_icon(tray_ui, tray_ui.desktop_index.get());
451    }
452    fn on_current_desktop_changed(&self, tray_ui: &Rc<SystemTray>, new_ix: u32) {
453        // Change icon first since any delay in that is more visible than if the
454        // tooltip isn't updated immediately:
455        self.update_tray_icon(tray_ui, new_ix);
456        const INDENT: &str = "           ";
457        self.tray.set_tip(&format!(
458            "Virtual Desktop Manager\
459            \n{INDENT}[Desktop {}]{name_preview}",
460            new_ix + 1,
461            name_preview = if let Some(name) = tray_ui
462                .get_desktop_name(new_ix)
463                .filter(|name| !name.trim().is_empty())
464            {
465                format!("\n{INDENT}[{name}]")
466            } else {
467                String::new()
468            }
469        ));
470    }
471    fn on_settings_changed(
472        &self,
473        tray_ui: &Rc<SystemTray>,
474        previous: &Arc<UiSettings>,
475        new: &Arc<UiSettings>,
476    ) {
477        if previous.tray_icon_type != new.tray_icon_type {
478            self.update_tray_icon(tray_ui, tray_ui.desktop_index.get());
479        }
480    }
481}
482
483/// Effect after a user presses a keyboard shortcut while a context menu is
484/// active.
485///
486/// # References
487///
488/// - <https://learn.microsoft.com/en-us/windows/win32/menurc/wm-menuchar>
489#[derive(Debug, Clone, Copy)]
490#[allow(dead_code)] // <- We might want to use the other alternatives in the future.
491pub enum MenuKeyPressEffect {
492    /// Discard the character the user pressed and create a short beep on the
493    /// system speaker
494    Ignore,
495    /// Close the active menu.
496    Close,
497    /// Choose the provided menu item and then close the menu.
498    Execute(nwg::ControlHandle),
499    /// Select the provided menu item.
500    Select(nwg::ControlHandle),
501    /// Select the menu item at the specified index of the current menu/submenu.
502    SelectIndex(usize),
503}
504
505/// A trait for Native GUI plugins for the system tray.
506pub trait TrayPlugin: DynamicUiHooks<SystemTray> {
507    /// React to Virtual Desktop events.
508    fn on_desktop_event(&self, _tray_ui: &Rc<SystemTray>, _event: &vd::DesktopEvent) {}
509    fn on_current_desktop_changed(&self, _tray_ui: &Rc<SystemTray>, _current_desktop_index: u32) {}
510    fn on_desktop_count_changed(&self, _tray_ui: &Rc<SystemTray>, _new_desktop_count: u32) {}
511
512    /// Handle keyboard button press on tray context menu. The first return
513    /// value that is `Some` will be used; if there is no such return value then
514    /// [`MenuKeyPressEffect::Ignore`] will be used.
515    ///
516    /// The provided `menu_handle` is the currently select parent menu.
517    fn on_menu_key_press(
518        &self,
519        _tray_ui: &Rc<SystemTray>,
520        _key_code: u32,
521        _menu_handle: isize,
522    ) -> Option<MenuKeyPressEffect> {
523        None
524    }
525
526    fn on_windows_mode_changed(&self, _tray_ui: &Rc<SystemTray>) {}
527
528    fn on_settings_changed(
529        &self,
530        _tray_ui: &Rc<SystemTray>,
531        _prev: &Arc<UiSettings>,
532        _new: &Arc<UiSettings>,
533    ) {
534    }
535}
536
537#[allow(dead_code)] // <- we might want to use some variants in the future
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum MenuPosition {
540    At(i32, i32),
541    AtPrevious,
542    AtTrayIcon,
543    AtMouseCursor,
544}
545
546pub type SystemTrayRef = DynamicUiRef<SystemTray>;
547
548/// Common handle used by tray plugins, usually behind an [`Rc`] stored inside
549/// [`SystemTrayRef`].
550#[derive(Debug)]
551pub struct SystemTray {
552    /// The total number of virtual desktops.
553    pub desktop_count: Cell<u32>,
554    /// The 0-based index of the currently active virtual desktop.
555    pub desktop_index: Cell<u32>,
556    /// Windows has separate modes for Windows itself and other apps. This
557    /// tracks whether the taskbar and Windows uses light colors.
558    has_light_taskbar: Cell<bool>,
559
560    desktop_names: RefCell<Vec<Option<Rc<str>>>>,
561
562    pub dynamic_ui: DynamicUi<Self>,
563}
564impl DynamicUiWrapper for SystemTray {
565    type Hooks = dyn TrayPlugin;
566
567    fn get_dynamic_ui(&self) -> &DynamicUi<Self> {
568        &self.dynamic_ui
569    }
570
571    fn get_dynamic_ui_mut(&mut self) -> &mut DynamicUi<Self> {
572        &mut self.dynamic_ui
573    }
574}
575/// Plugins.
576impl SystemTray {
577    pub fn new(mut plugins: Vec<Box<dyn TrayPlugin>>) -> Rc<Self> {
578        plugins.insert(0, Box::<TrayRoot>::default());
579        let has_light_taskbar = Self::check_if_light_taskbar();
580        tracing::debug!(
581            ?has_light_taskbar,
582            "Detected Windows mode (affects taskbar color)"
583        );
584        let dynamic_ui = DynamicUi::new(plugins);
585        dynamic_ui.set_prevent_recursive_events(false);
586        let this = Rc::new(Self {
587            desktop_count: Cell::new(vd::get_desktop_count().unwrap_or(1)),
588            desktop_index: Cell::new(1),
589            desktop_names: RefCell::new(Vec::new()),
590            has_light_taskbar: Cell::new(has_light_taskbar),
591
592            dynamic_ui,
593        });
594        this.update_desktop_info();
595        this
596    }
597    fn update_desktop_info(&self) {
598        self.desktop_index.set(
599            vd::get_current_desktop()
600                .and_then(|d| d.get_index())
601                .unwrap_or(1),
602        );
603        let names = vd::get_desktops()
604            .and_then(|ds| {
605                ds.into_iter()
606                    .map(|d| d.get_name().map(Rc::from).map(Some))
607                    .collect::<Result<Vec<_>, _>>()
608            })
609            .inspect_err(|e| {
610                tracing::warn!("Failed to get desktop names: {e:?}");
611            })
612            .unwrap_or_default();
613        self.desktop_names.replace(names);
614    }
615    /// # References
616    ///
617    /// - <https://stackoverflow.com/questions/56865923/windows-10-taskbar-color-detection-for-tray-icon>
618    /// - We use this function: [RegGetValueW in windows::Win32::System::Registry - Rust](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/System/Registry/fn.RegGetValueW.html)
619    ///   - Function docs: [RegGetValueW function (winreg.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-reggetvaluew)
620    ///   - StackOverflow usage example: [windows - RegGetValueW(), how to do it right - Stack Overflow](https://stackoverflow.com/questions/78224404/reggetvaluew-how-to-do-it-right)
621    fn check_if_light_taskbar() -> bool {
622        use windows::{
623            core::w,
624            Win32::System::Registry::{RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD},
625        };
626
627        let mut buffer: [u8; 4] = [0; 4];
628        let mut cb_data = buffer.len() as u32;
629        let res = unsafe {
630            RegGetValueW(
631                HKEY_CURRENT_USER,
632                w!(r#"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"#),
633                w!("SystemUsesLightTheme"),
634                RRF_RT_REG_DWORD,
635                Some(std::ptr::null_mut()),
636                Some(buffer.as_mut_ptr() as _),
637                Some(&mut cb_data as *mut u32),
638            )
639        };
640        if res.is_err() {
641            tracing::error!(
642                "Failed to read Windows mode from the registry: {:?}",
643                windows::core::Error::from(res.to_hresult())
644            );
645            return false;
646        }
647
648        // REG_DWORD is signed 32-bit, using little endian
649        let windows_light_mode = i32::from_le_bytes(buffer);
650        if ![0, 1].contains(&windows_light_mode) {
651            tracing::error!(
652                "Windows mode read from the registry was not 0 or 1 \
653                ({windows_light_mode}), ignoring read value"
654            );
655            return false;
656        }
657        windows_light_mode == 1
658    }
659    pub fn build_ui(self: Rc<Self>) -> Result<DynamicUiOwner<Self>, nwg::NwgError> {
660        <Rc<Self> as nwg::NativeUi<DynamicUiOwner<_>>>::build_ui(self)
661    }
662    pub fn root(&self) -> Ref<'_, TrayRoot> {
663        self.dynamic_ui
664            .get_ui::<TrayRoot>()
665            .expect("Accessed TrayRoot while it was being rebuilt")
666    }
667    pub fn settings(&self) -> Ref<'_, crate::settings::UiSettingsPlugin> {
668        self.dynamic_ui
669            .get_ui::<crate::settings::UiSettingsPlugin>()
670            .expect("Accessed UiSettingsPlugin while it was being rebuilt")
671    }
672    pub fn get_desktop_name(&self, index: u32) -> Option<Rc<str>> {
673        if vd::has_loaded_dynamic_library_successfully() {
674            // Don't get change events for desktop names, so just reload it
675            // every time we change desktop:
676            vd::get_desktop(index).get_name().ok().map(Rc::from)
677        } else {
678            self.desktop_names
679                .borrow()
680                .get(index as usize)
681                .cloned()
682                .flatten()
683        }
684    }
685    /// Windows has separate modes for Windows itself and other apps. This
686    /// tracks whether the taskbar and Windows uses light colors.
687    pub fn has_light_taskbar(&self) -> bool {
688        self.has_light_taskbar.get()
689    }
690}
691/// Events.
692impl SystemTray {
693    pub fn notify_quick_switch_hotkey(self: &Rc<Self>) {
694        if let Some(quick_switch_top_menu) = self
695            .get_dynamic_ui()
696            .get_ui::<crate::tray_plugins::menus::QuickSwitchTopMenu>()
697            .and_then(|plugin| plugin.menu_handle())
698        {
699            if let Some(open_submenu) = self
700                .get_dynamic_ui()
701                .get_ui::<crate::tray_plugins::menus::OpenSubmenuPlugin>()
702            {
703                open_submenu.queue_open_of([crate::tray_plugins::menus::SubMenu::Handle(
704                    quick_switch_top_menu,
705                )]);
706            } else {
707                tracing::warn!("Can't queue opening of submenu");
708            }
709        } else {
710            tracing::trace!("No top menu for quick switch menu");
711        }
712
713        self.show_menu(MenuPosition::AtTrayIcon);
714    }
715    pub fn notify_open_menu_at_mouse_position_hotkey(self: &Rc<Self>) {
716        self.reshow_menu(MenuPosition::AtMouseCursor)
717    }
718    pub fn notify_settings_changed(self: &Rc<Self>, prev: &Arc<UiSettings>, new: &Arc<UiSettings>) {
719        self.dynamic_ui
720            .for_each_ui(|plugin| plugin.on_settings_changed(self, prev, new));
721    }
722    fn notify_windows_mode_change(self: &Rc<Self>) {
723        let is_light = Self::check_if_light_taskbar();
724        let was_light = self.has_light_taskbar.replace(is_light);
725        tracing::info!(
726            ?was_light,
727            ?is_light,
728            "Windows changed its color mode (affects taskbar color)"
729        );
730        if is_light == was_light {
731            return;
732        }
733        self.dynamic_ui
734            .for_each_ui(|plugin| plugin.on_windows_mode_changed(self));
735    }
736    fn notify_explorer_restart(&self) {
737        tracing::warn!(
738            "Detected that Windows explorer.exe was restarted, attempting to re-register tray icon"
739        );
740        self.root().need_rebuild.set(true);
741    }
742    fn update_desktop_count(self: &Rc<Self>) {
743        match vd::get_desktop_count() {
744            Ok(count) => {
745                self.desktop_count.set(count);
746                {
747                    let len = self.desktop_names.borrow().len() as u32;
748                    match len.cmp(&count) {
749                        std::cmp::Ordering::Less => {
750                            let range = len..count;
751                            let new_names: Vec<_> = range.map(
752                                |ix| match vd::get_desktop(ix).get_name() {
753                                    Err(e) => {
754                                        tracing::warn!(
755                                            "Failed to get virtual desktop name for desktop {}: {e:?}",
756                                            ix + 1
757                                        );
758                                        None
759                                    }
760                                    Ok(name) => Some(Rc::from(name)),
761                                },
762                            ).collect();
763                            self.desktop_names.borrow_mut().extend(new_names);
764                        }
765                        std::cmp::Ordering::Greater => {
766                            self.desktop_names.borrow_mut().truncate(count as usize)
767                        }
768                        std::cmp::Ordering::Equal => {}
769                    }
770                }
771                self.dynamic_ui
772                    .for_each_ui(|plugin| plugin.on_desktop_count_changed(self, count));
773            }
774            Err(e) => tracing::error!("Failed to get virtual desktop count: {e:?}"),
775        }
776    }
777    pub fn notify_desktop_event(self: &Rc<Self>, event: vd::DesktopEvent) {
778        // Note: this will run inside an OnNotice event handler, so dynamic_ui
779        // will check for rebuilding afterwards.
780
781        use vd::DesktopEvent::*;
782
783        tracing::trace!("Desktop event: {:?}", event);
784
785        match &event {
786            DesktopCreated { .. } | DesktopDestroyed { .. } => self.update_desktop_count(),
787            DesktopNameChanged(d, new_name) => match d.get_index() {
788                Err(e) => {
789                    tracing::warn!("Failed to get virtual desktop index after name change: {e:?}");
790                }
791                Ok(ix) => {
792                    let mut names = self.desktop_names.borrow_mut();
793                    if let Some(name) = names.get_mut(ix as usize) {
794                        *name = Some(Rc::from(&**new_name));
795                    }
796                }
797            },
798            DesktopChanged { new, .. } => {
799                if let Ok(new_ix) = new.get_index() {
800                    if new_ix >= self.desktop_count.get() {
801                        tracing::warn!(
802                            new_index = new_ix,
803                            count = self.desktop_count.get(),
804                            "Switched to desktop index larger than desktop count, \
805                            must have failed initial load or missed change event"
806                        );
807                        self.update_desktop_count();
808                    }
809                    self.desktop_index.set(new_ix);
810                    self.dynamic_ui
811                        .for_each_ui(|plugin| plugin.on_current_desktop_changed(self, new_ix));
812                }
813            }
814            _ => {}
815        }
816
817        self.dynamic_ui
818            .for_each_ui(|plugin| plugin.on_desktop_event(self, &event));
819    }
820    fn notify_tray_left_click(&self) {
821        self.root().notify_that_tray_icon_exists();
822
823        let action = self.settings().get().left_click;
824        tracing::debug!(?action, "Left clicked tray icon");
825        match action {
826            TrayClickAction::Disabled => {}
827            TrayClickAction::StopFlashingWindows => {
828                self.stop_flashing_windows();
829            }
830            TrayClickAction::ToggleConfigurationWindow => {
831                self.configure_filters(false);
832            }
833            TrayClickAction::ApplyFilters => {
834                self.apply_filters();
835            }
836            TrayClickAction::OpenContextMenu => {
837                self.show_menu(MenuPosition::AtMouseCursor);
838            }
839        }
840    }
841    fn notify_tray_middle_click(&self) {
842        self.root().notify_that_tray_icon_exists();
843
844        let action = self.settings().get().middle_click;
845        tracing::debug!(?action, "Left clicked tray icon");
846        match action {
847            TrayClickAction::Disabled => {}
848            TrayClickAction::StopFlashingWindows => {
849                self.stop_flashing_windows();
850            }
851            TrayClickAction::ToggleConfigurationWindow => {
852                self.configure_filters(false);
853            }
854            TrayClickAction::ApplyFilters => {
855                self.apply_filters();
856            }
857            TrayClickAction::OpenContextMenu => {
858                self.show_menu(MenuPosition::AtMouseCursor);
859            }
860        }
861    }
862    fn notify_tray_menu_closed(&self) {
863        if let Some((queued_at, _menu_pos)) = self.root().reshow_tray_menu.get() {
864            if queued_at.elapsed() < Duration::from_millis(5000) {
865                // we need to wait a bit longer before we can show a context menu again:
866                self.root()
867                    .reshow_tray_menu_delay
868                    .notify_after(Duration::from_millis(50));
869                return;
870            }
871        }
872        // Attempt to give focus back to the most recent window:
873        self.hide_menu();
874        if let Some(plugin) = self.dynamic_ui.get_ui::<SmoothDesktopSwitcher>() {
875            plugin.refocus_last_window();
876        }
877    }
878    /// Note: this isn't run inside a `SystemTray::handle_action` callback and
879    /// so we might handle events while something is being rebuilt.
880    fn notify_key_press_in_menu(
881        self: &Rc<Self>,
882        key_code: u32,
883        active_menu_handle: isize,
884    ) -> Option<MenuKeyPressEffect> {
885        let mut first_res = None;
886        self.dynamic_ui.for_each_ui(|t| {
887            if let Some(res) = t.on_menu_key_press(self, key_code, active_menu_handle) {
888                if first_res.is_none() {
889                    first_res = Some(res);
890                }
891            }
892        });
893        first_res
894    }
895}
896/// Commands.
897impl SystemTray {
898    pub fn switch_desktop(&self, desktop_ix: u32) {
899        tracing::info!("SystemTray::switch_desktop({})", desktop_ix);
900        // TODO: store this in settings:
901        let smooth = self
902            .dynamic_ui
903            .get_ui::<crate::tray_plugins::menus::TopMenuItems>()
904            .map_or_else(
905                || {
906                    tracing::warn!("No TopMenuItems: can't check if smooth scroll is enabled");
907                    false
908                },
909                |top| top.tray_smooth_switch.checked(),
910            );
911
912        let desktop = vd::get_desktop(desktop_ix);
913        let res = 'result: {
914            if smooth {
915                if vd::switch_desktop_with_animation(desktop).is_ok() {
916                    tracing::debug!("Used COM interfaces to animate desktop switch");
917
918                    if let Some(plugin) = self.dynamic_ui.get_ui::<SmoothDesktopSwitcher>() {
919                        // Switching desktop with animation doesn't seem to
920                        // refocus the most recently used window like it does
921                        // when animations aren't used, so we do it manually:
922                        plugin.refocus_last_window();
923                    }
924                } else if let Some(plugin) = self.dynamic_ui.get_ui::<SmoothDesktopSwitcher>() {
925                    // Attempt to hide menu since its closing animation doesn't
926                    // look nice when smoothly switching desktop:
927                    self.hide_menu();
928                    //crate::invisible_window::switch_desktop_with_invisible_window(desktop, Some(self.window.handle))
929                    break 'result plugin.switch_desktop_to(desktop);
930                }
931                tracing::warn!("No SmoothDesktopSwitcher: can't execute smooth scroll");
932            }
933            vd::switch_desktop(desktop)
934        };
935        if let Err(e) = res {
936            self.show_notification(
937                "Virtual Desktop Manager Error",
938                &format!(
939                    "Failed switch to Virtual Desktop {}: {e:?}",
940                    desktop_ix.saturating_add(1)
941                ),
942            );
943        }
944    }
945    /// This doesn't seem to actually do anything, needs to be changed to
946    /// actually work.
947    fn hide_menu(&self) {
948        tracing::info!("SystemTray::hide_menu()");
949        use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_HIDE};
950        unsafe {
951            // https://stackoverflow.com/questions/19226173/how-to-close-a-context-menu-after-a-timeout
952            // https://learn.microsoft.com/sv-se/windows/win32/winmsg/wm-cancelmode?redirectedfrom=MSDN
953            let _ = ShowWindow(
954                HWND(self.root().tray_menu.handle.pop_hmenu().unwrap().1.cast()),
955                SW_HIDE,
956            );
957        }
958    }
959    pub fn show_menu(&self, position: MenuPosition) {
960        let root = self.root();
961        let (x, y) = match position {
962            MenuPosition::At(x, y) => (x, y),
963            MenuPosition::AtPrevious => root
964                .last_menu_pos
965                .get()
966                .unwrap_or_else(nwg::GlobalCursor::position),
967            MenuPosition::AtTrayIcon => match tray_get_rect(&root.tray) {
968                Ok(rect) => ((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2),
969                Err(e) => {
970                    tracing::error!("Failed to get tray location: {e}");
971                    nwg::GlobalCursor::position()
972                }
973            },
974            MenuPosition::AtMouseCursor => nwg::GlobalCursor::position(),
975        };
976        tracing::info!(
977            actual_position = ?(x, y),
978            requested_position = ?position,
979            tray_location = ?tray_get_rect(&root.tray),
980            cursor_position = ?nwg::GlobalCursor::position(),
981            previous_pos = ?root.last_menu_pos.get(),
982            "SystemTray::show_menu()"
983        );
984
985        if let Some(plugin) = self.dynamic_ui.get_ui::<SmoothDesktopSwitcher>() {
986            plugin.cancel_refocus();
987        }
988        root.last_menu_pos.set(Some((x, y)));
989        root.tray_menu.popup(x, y);
990    }
991    /// Close and then re-open the context menu to ensure it is opened at the requested position.
992    pub fn reshow_menu(&self, position: MenuPosition) {
993        if self.close_menu() {
994            tracing::info!(?position, "Queued re-show of context menu");
995            self.root()
996                .reshow_tray_menu
997                .set(Some((Instant::now(), position)));
998        } else {
999            self.show_menu(position);
1000        }
1001    }
1002    /// Close the context menu if it is open. Returns `true` if an existing menu
1003    /// was closed.
1004    ///
1005    /// Note: the menu won't be closed until you return to the event loop. This
1006    /// means that re-opening the menu immediately after will not work.
1007    pub fn close_menu(&self) -> bool {
1008        tracing::info!("Close context menu");
1009
1010        use windows::Win32::UI::WindowsAndMessaging::CloseWindow;
1011
1012        let Some(context_menu_window) = crate::nwg_ext::find_context_menu_window() else {
1013            return false;
1014        };
1015
1016        if let Err(e) = unsafe { CloseWindow(context_menu_window) } {
1017            tracing::error!("Failed to close context menu: {e}");
1018        }
1019        true
1020    }
1021    pub fn show_notification(&self, title: &str, text: &str) {
1022        let flags = nwg::TrayNotificationFlags::USER_ICON | nwg::TrayNotificationFlags::LARGE_ICON;
1023        self.root()
1024            .tray
1025            .show(text, Some(title), Some(flags), Some(&self.root().icon));
1026    }
1027    pub fn apply_filters(&self) {
1028        tracing::info!("SystemTray::apply_filters()");
1029        if let Some(apply_filters) = self
1030            .get_dynamic_ui()
1031            .get_ui::<crate::tray_plugins::apply_filters::ApplyFilters>()
1032        {
1033            let settings = self.settings().get();
1034            let filters = settings.filters.clone();
1035            apply_filters.apply_filters(
1036                filters,
1037                settings.stop_flashing_windows_after_applying_filter,
1038            );
1039        } else {
1040            self.show_notification(
1041                "Virtual Desktop Manager Warning",
1042                "Applying filters is not supported",
1043            );
1044        }
1045    }
1046    pub fn configure_filters(&self, refocus: bool) {
1047        tracing::info!("SystemTray::configure_filters()");
1048        if let Some(config_window) = self.dynamic_ui.get_ui::<ConfigWindow>() {
1049            if config_window.is_closed() {
1050                config_window.open_soon.set(true);
1051            } else if refocus {
1052                config_window.set_as_foreground_window();
1053            } else {
1054                config_window.window.close();
1055            }
1056        }
1057    }
1058    pub fn stop_flashing_windows(&self) {
1059        let guard = self
1060            .get_dynamic_ui()
1061            .get_ui::<crate::tray_plugins::apply_filters::ApplyFilters>();
1062        if let Some(background) = guard {
1063            background.stop_all_flashing_windows();
1064        } else {
1065            self.show_notification(
1066                "Virtual Desktop Manager Warning",
1067                "Stopping flashing windows is not supported",
1068            );
1069        }
1070    }
1071
1072    pub fn exit(&self) {
1073        tracing::info!("SystemTray::exit()");
1074        nwg::stop_thread_dispatch();
1075    }
1076}