1use 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#[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 #[nwg_resource(source_embed: Some(&nwg::EmbedResource::load(None).unwrap()), source_embed_id: 1)]
37 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 )]
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 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 #[nwg_control(parent: window)]
70 #[nwg_events(OnNotice: [Self::notify_startup_rebuild])]
71 rebuild_at_startup: FastTimerControl,
72 first_created_at: Option<Instant>,
74
75 #[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 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 let Some(tray_ui) = self.tray_ui.get() else {
117 return;
118 };
119 if tray_ui.desktop_count.get() <= 1 {
120 if matches!(vd::get_desktop_count(), Err(_) | Ok(0 | 1))
122 && 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 tray_set_version_4(&self.tray);
209
210 windows_msg_for_explorer_restart();
212
213 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 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 const NIN_KEYSELECT: u32 = NINF_KEY | NIN_SELECT;
281
282 const NWG_TRAY: u32 = WM_USER + 102;
288
289 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 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 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 return Some(2 << 16 | item_index);
360 } else {
361 tracing::debug!(?effect, "Selecting menu item at index {item_index}");
362 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 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 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 NIN_SELECT => {}
400 NIN_KEYSELECT => {
402 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 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 }
422 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 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 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#[derive(Debug, Clone, Copy)]
490#[allow(dead_code)] pub enum MenuKeyPressEffect {
492 Ignore,
495 Close,
497 Execute(nwg::ControlHandle),
499 Select(nwg::ControlHandle),
501 SelectIndex(usize),
503}
504
505pub trait TrayPlugin: DynamicUiHooks<SystemTray> {
507 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 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)] #[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#[derive(Debug)]
551pub struct SystemTray {
552 pub desktop_count: Cell<u32>,
554 pub desktop_index: Cell<u32>,
556 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}
575impl 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 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 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 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 pub fn has_light_taskbar(&self) -> bool {
688 self.has_light_taskbar.get()
689 }
690}
691impl 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 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 self.root()
867 .reshow_tray_menu_delay
868 .notify_after(Duration::from_millis(50));
869 return;
870 }
871 }
872 self.hide_menu();
874 if let Some(plugin) = self.dynamic_ui.get_ui::<SmoothDesktopSwitcher>() {
875 plugin.refocus_last_window();
876 }
877 }
878 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}
896impl SystemTray {
898 pub fn switch_desktop(&self, desktop_ix: u32) {
899 tracing::info!("SystemTray::switch_desktop({})", desktop_ix);
900 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 plugin.refocus_last_window();
923 }
924 } else if let Some(plugin) = self.dynamic_ui.get_ui::<SmoothDesktopSwitcher>() {
925 self.hide_menu();
928 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 fn hide_menu(&self) {
948 tracing::info!("SystemTray::hide_menu()");
949 use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_HIDE};
950 unsafe {
951 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 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 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}