virtual_desktop_manager/
settings.rs

1use crate::{
2    dynamic_gui::DynamicUiHooks,
3    tray::{SystemTray, SystemTrayRef, TrayPlugin, TrayRoot},
4    window_filter::WindowFilter,
5};
6#[cfg(feature = "persist_settings")]
7use serde::{Deserialize, Deserializer, Serialize};
8use std::{
9    any::TypeId,
10    cell::Cell,
11    collections::BTreeMap,
12    fmt,
13    ops::Deref,
14    path::Path,
15    rc::Rc,
16    sync::{Arc, Condvar, Mutex},
17};
18#[cfg(feature = "persist_settings")]
19use std::{
20    cell::OnceCell,
21    io::{ErrorKind::NotFound, Write},
22    sync::{mpsc, MutexGuard},
23    time::Duration,
24};
25
26/// Use a default value if serialization fails for a field.
27///
28/// # References
29///
30/// Inspired by:
31///
32/// [\[Solved\] Serde deserialization on_error use default values? - help - The
33/// Rust Programming Language
34/// Forum](https://users.rust-lang.org/t/solved-serde-deserialization-on-error-use-default-values/6681)
35#[cfg(feature = "persist_settings")]
36fn ok_or_none<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
37where
38    T: Deserialize<'de>,
39    D: Deserializer<'de>,
40{
41    let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
42    Ok(T::deserialize(v).ok())
43}
44
45/// Provide a deserialization for `UiSettings` that can handle malformed fields.
46macro_rules! default_deserialize {
47    (@inner
48        $(#[$ty_attr:meta])*
49        $ty_vis:vis struct $name:ident { $(
50            $(#[$field_attr:meta])*
51            $field_vis:vis $field_name:ident: $field_ty:ty
52        ,)* $(,)? }
53    ) => {
54        #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
55        #[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
56        struct UiSettingsFallback { $(
57            $(#[$field_attr])*
58            #[cfg_attr(feature = "persist_settings", serde(deserialize_with = "ok_or_none"))] // None if deserialization failed
59            #[cfg_attr(feature = "persist_settings", serde(default))] // None if field isn't present
60            $field_vis $field_name: Option<$field_ty>,
61        )* }
62        impl UiSettingsFallback {
63            /// `true` if all fields have values and so all errors have been fixed.
64            pub fn has_all_fields(&self) -> bool {
65                $(
66                    self.$field_name.is_some()
67                )&&*
68            }
69        }
70        impl From<UiSettingsFallback> for $name {
71            fn from(value: UiSettingsFallback) -> Self {
72                let mut this = <Self as Default>::default();
73                $(
74                    if let Some($field_name) = value.$field_name {
75                        this.$field_name = $field_name;
76                    }
77                )*
78                this
79            }
80        }
81    };
82    ($($token:tt)*) => {
83        $($token)*
84        default_deserialize! { @inner $($token)* }
85    };
86}
87
88#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
89#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
90#[cfg_attr(feature = "persist_settings", serde(rename_all = "lowercase"))]
91#[allow(dead_code)]
92pub enum AutoStart {
93    #[default]
94    Disabled,
95    Enabled,
96    Elevated,
97}
98impl AutoStart {
99    pub const ALL: &'static [Self] = &[
100        Self::Disabled,
101        // TODO: Add support for auto start without admin rights
102        // Self::Enabled,
103        Self::Elevated,
104    ];
105}
106/// Used to display options in config window.
107impl fmt::Display for AutoStart {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        let text = match *self {
110            AutoStart::Disabled => "No",
111            AutoStart::Enabled => "Yes",
112            AutoStart::Elevated => "Yes, with admin rights",
113        };
114        f.write_str(text)
115    }
116}
117
118#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
119#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
120#[allow(dead_code)]
121pub enum QuickSwitchMenu {
122    Disabled,
123    TopMenu,
124    #[default]
125    SubMenu,
126}
127impl QuickSwitchMenu {
128    pub const ALL: &'static [Self] = &[Self::Disabled, Self::TopMenu, Self::SubMenu];
129}
130/// Used to display options in config window.
131impl fmt::Display for QuickSwitchMenu {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        let text = match *self {
134            QuickSwitchMenu::Disabled => "Off",
135            QuickSwitchMenu::TopMenu => "Inside the main context menu",
136            QuickSwitchMenu::SubMenu => "Inside a submenu",
137        };
138        f.write_str(text)
139    }
140}
141
142#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
143#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
144#[allow(dead_code)]
145pub enum TrayIconType {
146    /// Show an icon that has a frame around the desktop index.
147    ///
148    /// This allows using hardcoded icons for low desktop indexes if available
149    /// which should have higher quality and be faster to load.
150    #[default]
151    WithBackground,
152    /// Show an icon that has a frame around the desktop index, but don't use
153    /// hardcoded icons for low desktop indexes.
154    WithBackgroundNoHardcoded,
155    /// Show an icon with only a desktop index and no frame or anything else.
156    /// The desktop index is rendered using the `imageproc` crate.
157    NoBackground,
158    /// Show an icon with only a desktop index and no frame or anything else.
159    /// The desktop index is rendered using the `text_to_png` crate.
160    NoBackground2,
161    /// Show the same icon as the executable.
162    AppIcon,
163}
164impl TrayIconType {
165    pub const ALL: &'static [Self] = &[
166        #[cfg(feature = "tray_icon_hardcoded")]
167        Self::WithBackground,
168        #[cfg(feature = "tray_icon_with_background")]
169        Self::WithBackgroundNoHardcoded,
170        #[cfg(feature = "tray_icon_text_only")]
171        Self::NoBackground,
172        #[cfg(feature = "tray_icon_text_only_alt")]
173        Self::NoBackground2,
174        Self::AppIcon,
175    ];
176}
177/// Used to display options in config window.
178impl fmt::Display for TrayIconType {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        let text = match *self {
181            TrayIconType::WithBackground => "Hardcoded number inside icon",
182            TrayIconType::WithBackgroundNoHardcoded => "Generated number inside icon",
183            TrayIconType::NoBackground => "Only black and white number",
184            TrayIconType::NoBackground2 => "Only purple number",
185            TrayIconType::AppIcon => "Only program icon, no number",
186        };
187        f.write_str(text)
188    }
189}
190
191#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
192#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
193#[allow(dead_code)]
194pub enum TrayClickAction {
195    #[default]
196    Disabled,
197    StopFlashingWindows,
198    ToggleConfigurationWindow,
199    ApplyFilters,
200    OpenContextMenu,
201}
202impl TrayClickAction {
203    pub const ALL: &'static [Self] = &[
204        Self::Disabled,
205        Self::StopFlashingWindows,
206        Self::ToggleConfigurationWindow,
207        Self::ApplyFilters,
208        Self::OpenContextMenu,
209    ];
210}
211/// Used to display options in config window.
212impl fmt::Display for TrayClickAction {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        let text = match *self {
215            Self::Disabled => "Disabled",
216            Self::StopFlashingWindows => "Stop Flashing Windows",
217            Self::ToggleConfigurationWindow => "Open/Close Config Window",
218            Self::ApplyFilters => "Apply Filters",
219            Self::OpenContextMenu => "Open Context Menu",
220        };
221        f.write_str(text)
222    }
223}
224
225#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
226#[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
227pub struct ConfigWindowInfo {
228    pub position: Option<(i32, i32)>,
229    pub size: (u32, u32),
230    pub maximized: bool,
231}
232impl Default for ConfigWindowInfo {
233    fn default() -> Self {
234        Self {
235            position: None,
236            size: (800, 600),
237            maximized: false,
238        }
239    }
240}
241
242default_deserialize!(
243    #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
244    #[cfg_attr(feature = "persist_settings", derive(Serialize, Deserialize))]
245    pub struct UiSettings {
246        pub version: u64,
247        /// Autostart this program when Windows is started.
248        pub auto_start: AutoStart,
249        /// If this is enabled then we will attempt to switch virtual desktops
250        /// using animations. This is done by opening a transparent window on a
251        /// different virtual desktop and then focusing on it.
252        pub smooth_switch_desktops: bool,
253        /// Elevated permission is useful to move windows owned by elevated
254        /// programs. If this setting is enabled the program will ask for admin
255        /// rights every time it is started, making it easy to always have them.
256        pub request_admin_at_startup: bool,
257        /// Some windows might be trying to grab the user's attention and will
258        /// be flashing in the taskbar. These taskbar items will remain visible
259        /// even if the window is moved to another virtual desktop. If this
260        /// setting is enabled we will attempt to stop windows from flashing in
261        /// the taskbar after moving them.
262        pub stop_flashing_windows_after_applying_filter: bool,
263        /// The type of icon to show in the system tray.
264        pub tray_icon_type: TrayIconType,
265        /// Fancy context menu items that allows switching to a desktop by
266        /// entering its one-based index via context menu keyboard shortcuts.
267        pub quick_switch_menu: QuickSwitchMenu,
268        /// Extra context menu items that have custom access keys to allow fast
269        /// switching to specific desktops. Usually this is used if you have
270        /// more than 9 desktops because then pressing `1` could be interpreted
271        /// as the start of `10` and so it is useful to have another key that
272        /// brings you to the first desktop.
273        pub quick_switch_menu_shortcuts: Arc<BTreeMap<String, u32>>,
274        /// Determines if the extra shortcut menu items should be shown even in
275        /// submenus of the quick switch menu. Usually it is enough to only have
276        /// them in the top most "quick switch" context menu.
277        pub quick_switch_menu_shortcuts_only_in_root: bool,
278
279        /// Global keyboard shortcut for opening the quick switch menu. Will be
280        /// parsed as a [`global_hotkey::hotkey::HotKey`].
281        pub quick_switch_hotkey: Arc<str>,
282
283        /// Global keyboard shortcut for opening the context menu at the mouse's
284        /// current position. Quite useful when the keyboard shortcut is used by
285        /// a macro triggered by a mouse button.
286        pub open_menu_at_mouse_pos_hotkey: Arc<str>,
287
288        pub left_click: TrayClickAction,
289        /// Middle clicks are registered as left clicks for at least some
290        /// versions of Windows 11.
291        pub middle_click: TrayClickAction,
292
293        /// Info about last location of the configuration window.
294        pub config_window: ConfigWindowInfo,
295        /// Filters/rules that specify which windows should be moved and to what
296        /// virtual desktop.
297        pub filters: Arc<[WindowFilter]>,
298    }
299);
300impl UiSettings {
301    const CURRENT_VERSION: u64 = 2;
302
303    /// Ensure settings are the newest version. Some work might have been done
304    /// previously by [`UiSettingsFallback::maybe_migrate`] if initial parsing
305    /// failed.
306    fn migrate(&mut self) {
307        // Always change the version to latest, since if we save the data this
308        // is the version that will be written:
309        self.version = Self::CURRENT_VERSION;
310    }
311}
312impl UiSettingsFallback {
313    /// Handle some migrations to newer setting formats. If all errors could be
314    /// explained by version mismatch then returns `true`.
315    fn maybe_migrate(&mut self) -> bool {
316        if self.open_menu_at_mouse_pos_hotkey.is_none() && matches!(self.version, Some(v) if v <= 1) {
317            self.open_menu_at_mouse_pos_hotkey = Some(Arc::from(""));
318        }
319        self.has_all_fields()
320    }
321}
322impl Default for UiSettings {
323    fn default() -> Self {
324        Self {
325            version: Self::CURRENT_VERSION,
326            auto_start: AutoStart::default(),
327            smooth_switch_desktops: true,
328            request_admin_at_startup: false,
329            stop_flashing_windows_after_applying_filter: false,
330            tray_icon_type: TrayIconType::default(),
331            quick_switch_menu: QuickSwitchMenu::default(),
332            quick_switch_menu_shortcuts: Arc::new(BTreeMap::from([
333                // Useful when using the numpad:
334                (",".to_owned(), 0),
335            ])),
336            quick_switch_menu_shortcuts_only_in_root: false,
337            quick_switch_hotkey: Arc::from(""),
338            open_menu_at_mouse_pos_hotkey: Arc::from(""),
339
340            left_click: TrayClickAction::ToggleConfigurationWindow,
341            middle_click: TrayClickAction::ApplyFilters,
342
343            config_window: ConfigWindowInfo::default(),
344            filters: Arc::new([]),
345        }
346    }
347}
348
349#[cfg(feature = "persist_settings")]
350struct UiState {
351    error_notice: nwg::NoticeSender,
352    error_tx: mpsc::Sender<String>,
353    thread_join: std::thread::JoinHandle<()>,
354}
355
356struct UiSettingsPluginState {
357    settings: Arc<UiSettings>,
358    #[cfg(feature = "persist_settings")]
359    settings_in_file: Arc<UiSettings>,
360    save_path: Option<Arc<Path>>,
361    temp_save_path: Option<Arc<Path>>,
362    #[cfg(feature = "persist_settings")]
363    should_close: bool,
364    #[cfg(feature = "persist_settings")]
365    ui_state: Option<UiState>,
366}
367impl Default for UiSettingsPluginState {
368    fn default() -> Self {
369        let settings = Arc::new(UiSettings::default());
370        #[cfg(feature = "persist_settings")]
371        let settings_in_file = Arc::clone(&settings);
372        Self {
373            settings,
374            #[cfg(feature = "persist_settings")]
375            settings_in_file,
376            #[cfg(feature = "persist_settings")]
377            should_close: false,
378            #[cfg(feature = "persist_settings")]
379            ui_state: None,
380            save_path: None,
381            temp_save_path: None,
382        }
383    }
384}
385
386#[derive(Default)]
387struct UiSettingsPluginShared {
388    state: Mutex<UiSettingsPluginState>,
389    /// Background thread waits on this when it is allowed to save settings.
390    notify_change: Condvar,
391    /// Background thread waits on this when its not allowed to save settings
392    /// for a while.
393    #[cfg(feature = "persist_settings")]
394    notify_close: Condvar,
395}
396#[cfg(feature = "persist_settings")]
397impl UiSettingsPluginShared {
398    fn close_background_thread(this: &Self, mut guard: MutexGuard<UiSettingsPluginState>) {
399        guard.should_close = true;
400        let ui_state = guard.ui_state.take();
401        drop(guard);
402        this.notify_change.notify_all();
403        this.notify_close.notify_all();
404        if let Some(ui_state) = ui_state {
405            ui_state.thread_join.join().unwrap();
406        }
407    }
408    fn start_background_work(
409        self: &Arc<Self>,
410        error_notice: nwg::NoticeSender,
411        error_tx: mpsc::Sender<String>,
412    ) {
413        let mut guard = self.state.lock().unwrap();
414
415        // Finish closing threads:
416        while guard.should_close && guard.ui_state.is_some() {
417            Self::close_background_thread(self, guard);
418            guard = self.state.lock().unwrap();
419        }
420        guard.should_close = false;
421
422        // If there is a running thread then don't start a new one:
423        if let Some(ui_state) = &mut guard.ui_state {
424            ui_state.error_notice = error_notice;
425            ui_state.error_tx = error_tx;
426            return;
427        }
428
429        // Start new background thread:
430        let thread_join = std::thread::Builder::new()
431            .name("UiSettingsSaveThread".to_owned())
432            .spawn({
433                let shared = Arc::clone(self);
434                move || shared.background_work()
435            })
436            .expect("Failed to spawn thread for saving UI settings");
437        guard.ui_state = Some(UiState {
438            error_notice,
439            error_tx,
440            thread_join,
441        });
442    }
443    fn background_work(self: Arc<Self>) {
444        let mut guard = self.state.lock().unwrap();
445        let mut latest_saved;
446        while !guard.should_close {
447            latest_saved = Arc::clone(&guard.settings);
448            let result = self.save_settings_inner(guard);
449            guard = self.state.lock().unwrap();
450            if guard.should_close {
451                return;
452            }
453            match result {
454                Ok(true) => {
455                    // Saved data => wait a while before we try saving again:
456                    guard = self
457                        .notify_close
458                        .wait_timeout(guard, Duration::from_millis(1000))
459                        .unwrap()
460                        .0;
461                    if guard.should_close {
462                        return;
463                    }
464                }
465                Ok(false) => {
466                    // No changes => wait until new settings data is specified
467                }
468                Err(e) => {
469                    tracing::error!(?e, "Failed to save UI settings");
470                    if let Some(ui_state) = &guard.ui_state {
471                        if let Err(e) = ui_state.error_tx.send(e) {
472                            tracing::warn!(error = ?e, "Failed to send UiSettings save error to UI thread");
473                        }
474                        ui_state.error_notice.notice();
475                    }
476                }
477            }
478
479            // Wait until new settings data is specified:
480            if Arc::ptr_eq(&latest_saved, &guard.settings) {
481                guard = self.notify_change.wait(guard).unwrap();
482                if guard.should_close {
483                    return;
484                }
485            }
486
487            // Attempt to batch save changes by waiting a little before saving:
488            guard = self
489                .notify_close
490                .wait_timeout(guard, Duration::from_millis(50))
491                .unwrap()
492                .0;
493        }
494    }
495
496    fn save_settings_inner(
497        &self,
498        mut guard: MutexGuard<UiSettingsPluginState>,
499    ) -> Result<bool, String> {
500        if Arc::ptr_eq(&guard.settings, &guard.settings_in_file) {
501            return Ok(false);
502        }
503        if guard.settings == guard.settings_in_file {
504            // Ensure there is a single allocation for the UI settings:
505            guard.settings_in_file = Arc::clone(&guard.settings);
506            return Ok(false);
507        }
508        let new_data = guard.settings.clone();
509
510        let Some(save_path) = guard.save_path.clone() else {
511            tracing::warn!("Can't save settings since there was no save path");
512            return Ok(false);
513        };
514        let Some(temp_path) = guard.temp_save_path.clone() else {
515            tracing::warn!("Can't save settings since there was no temporary save path");
516            return Ok(false);
517        };
518        // Don't hold lock during slow operations:
519        drop(guard);
520
521        tracing::trace!(?save_path, ?temp_path, ?new_data, "Saving UI settings");
522
523        let binary_data = serde_json::to_vec_pretty(&*new_data)
524            .map_err(|e| format!("Failed to serialize UI settings: {e}"))?;
525
526        match std::fs::remove_file(&temp_path) {
527            Ok(_) => {}
528            Err(e) if e.kind() == NotFound => {}
529            Err(e) => {
530                return Err(format!("Failed to remove temp ui settings: {e}"));
531            }
532        }
533
534        {
535            let mut file = std::fs::OpenOptions::new()
536                .create_new(true)
537                .truncate(true)
538                .write(true)
539                .open(&temp_path)
540                .map_err(|e| format!("Failed to create new UI settings file: {e}"))?;
541
542            file.write_all(&binary_data)
543                .map_err(|e| format!("Failed to write UI settings to file: {e}"))?;
544
545            file.flush()
546                .map_err(|e| format!("Failed to flush UI settings to file: {e}"))?;
547        }
548
549        std::fs::rename(&temp_path, &save_path)
550            .map_err(|e| format!("Failed to rename new UI settings file: {e}"))?;
551
552        let mut guard = self.state.lock().unwrap();
553        guard.settings_in_file = new_data;
554
555        Ok(true)
556    }
557}
558
559#[derive(Default)]
560struct UiSettingsPluginSharedStrong(Arc<UiSettingsPluginShared>);
561#[cfg(feature = "persist_settings")]
562impl Drop for UiSettingsPluginSharedStrong {
563    fn drop(&mut self) {
564        if let Ok(guard) = self.0.state.lock() {
565            UiSettingsPluginShared::close_background_thread(&self.0, guard);
566        }
567    }
568}
569impl Deref for UiSettingsPluginSharedStrong {
570    type Target = Arc<UiSettingsPluginShared>;
571    fn deref(&self) -> &Self::Target {
572        &self.0
573    }
574}
575
576/// This plugin tracks UI settings.
577#[derive(nwd::NwgPartial, Default)]
578pub struct UiSettingsPlugin {
579    tray_ui: SystemTrayRef,
580    #[cfg(feature = "persist_settings")]
581    #[nwg_control]
582    #[nwg_events(OnNotice: [Self::on_background_error])]
583    error_notice: nwg::Notice,
584    #[cfg(feature = "persist_settings")]
585    error_rx: OnceCell<mpsc::Receiver<String>>,
586    load_error: Cell<Option<String>>,
587    shared: UiSettingsPluginSharedStrong,
588}
589impl UiSettingsPlugin {
590    pub fn get(&self) -> Arc<UiSettings> {
591        Arc::clone(&self.shared.state.lock().unwrap().settings)
592    }
593    pub fn set(&self, value: UiSettings) {
594        let new;
595        let prev = {
596            let mut state = self.shared.state.lock().unwrap();
597            if *state.settings == value {
598                return;
599            }
600            new = Arc::new(value);
601            let prev = std::mem::replace(&mut state.settings, Arc::clone(&new));
602            self.shared.notify_change.notify_all();
603            prev
604        };
605        if let Some(tray) = self.tray_ui.get() {
606            tray.notify_settings_changed(&prev, &new);
607        }
608    }
609    pub fn update(&self, f: impl FnOnce(&UiSettings) -> UiSettings) {
610        let current = self.get();
611        let new = f(&current);
612        drop(current);
613        self.set(new);
614    }
615
616    pub fn with_save_path_next_to_exe() -> Self {
617        let mut this = Self::default();
618        this.set_save_path_next_to_exe();
619        this
620    }
621    pub fn set_save_path_next_to_exe(&mut self) {
622        let exe_path = match std::env::current_exe() {
623            Ok(v) => v,
624            Err(e) => {
625                self.load_error.set(Some(format!(
626                    "Failed to find UI settings file, can't get executable's path: {e}"
627                )));
628                return;
629            }
630        };
631        {
632            let mut guard = self.shared.state.lock().unwrap();
633            guard.save_path = Some(Arc::from(exe_path.with_extension("settings.json")));
634            guard.temp_save_path = Some(Arc::from(exe_path.with_extension("settings.temp.json")));
635        }
636        self.load_data();
637    }
638    pub fn load_data(&self) {
639        #[cfg(feature = "persist_settings")]
640        {
641            let Some(save_path) = self.shared.state.lock().unwrap().save_path.clone() else {
642                return;
643            };
644            let (settings, load_error) = match std::fs::read_to_string(&save_path) {
645                Ok(data) => {
646                    let mut deserializer = serde_json::Deserializer::from_str(&data);
647                    let result: Result<UiSettings, _> = {
648                        #[cfg(not(feature = "serde_path_to_error"))]
649                        {
650                            serde::Deserialize::deserialize(&mut deserializer)
651                        }
652                        #[cfg(feature = "serde_path_to_error")]
653                        {
654                            serde_path_to_error::deserialize(&mut deserializer)
655                        }
656                    };
657                    match result {
658                        Ok(settings) => (Some(settings), None),
659                        Err(e) => {
660                            let mut ignore_error = false;
661                            (
662                            // Try to be more lenient when parsing (skip parsing for
663                            // fields that fail and use default values for those):
664                            serde_json::from_str::<UiSettingsFallback>(&data)
665                                .ok()
666                                .map(|mut fallback| {
667                                    ignore_error = fallback.maybe_migrate();
668                                    UiSettings::from(fallback)
669                                }),
670                            // Emit an error message for why the strict parsing failed:
671                            Some(format!(
672                                "Could not parse UI settings file as JSON: {e}: Settings file at \"{}\"",
673                                save_path.display()
674                            )).filter(|_| !ignore_error),
675                        )
676                        }
677                    }
678                }
679                Err(e) if e.kind() == NotFound => {
680                    tracing::trace!(
681                        "Using default settings since no UI settings file was found at \"{}\"",
682                        save_path.display()
683                    );
684                    (None, None)
685                }
686                Err(e) => (
687                    None,
688                    Some(format!(
689                        "Failed to read UI settings file: {e}: Settings file at \"{}\"",
690                        save_path.display()
691                    )),
692                ),
693            };
694            // Notify error:
695            if let Some(error) = load_error {
696                if let Some(tray) = self.tray_ui.get() {
697                    Self::notify_load_error(&tray, &error)
698                } else {
699                    self.load_error.set(Some(error));
700                }
701            }
702            // Update tracked settings:
703            if let Some(mut settings) = settings {
704                settings.migrate();
705                let new = Arc::new(settings);
706                let prev = {
707                    let mut state = self.shared.state.lock().unwrap();
708                    state.settings_in_file = Arc::clone(&new);
709                    std::mem::replace(&mut state.settings, Arc::clone(&new))
710                };
711                if let Some(tray) = self.tray_ui.get() {
712                    tray.notify_settings_changed(&prev, &new);
713                }
714            }
715        }
716    }
717    fn notify_load_error(tray_ui: &SystemTray, error: &str) {
718        tray_ui.show_notification("Virtual Desktop Manager Error", error);
719    }
720    #[cfg(feature = "persist_settings")]
721    fn on_background_error(&self) {
722        let Some(error_rx) = self.error_rx.get() else {
723            return;
724        };
725        let Some(dynamic_ui) = self.tray_ui.get() else {
726            return;
727        };
728        for error in error_rx.try_iter() {
729            dynamic_ui.show_notification("Virtual Desktop Manager Error", &error);
730        }
731    }
732}
733impl DynamicUiHooks<SystemTray> for UiSettingsPlugin {
734    fn before_partial_build(
735        &mut self,
736        tray_ui: &Rc<SystemTray>,
737        _should_build: &mut bool,
738    ) -> Option<(nwg::ControlHandle, TypeId)> {
739        self.tray_ui.set(tray_ui);
740        Some((tray_ui.root().window.handle, TypeId::of::<TrayRoot>()))
741    }
742    fn after_partial_build(&mut self, tray_ui: &Rc<SystemTray>) {
743        if let Some(error) = self.load_error.take() {
744            Self::notify_load_error(tray_ui, &error);
745        }
746
747        #[cfg(feature = "persist_settings")]
748        {
749            let (tx, rx) = mpsc::channel();
750            self.shared
751                .start_background_work(self.error_notice.sender(), tx);
752            if self.error_rx.set(rx).is_err() {
753                tracing::error!("Failed to set new error receiver for UiSettingsPlugin");
754            }
755        }
756    }
757    fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
758        self.tray_ui = Default::default();
759        #[cfg(feature = "persist_settings")]
760        {
761            self.error_notice = Default::default();
762            self.error_rx = OnceCell::new();
763        }
764    }
765}
766impl TrayPlugin for UiSettingsPlugin {}