1use std::{
2 cell::{Cell, OnceCell, RefCell},
3 cmp::Ordering,
4 collections::BTreeMap,
5 fs::OpenOptions,
6 io::Write,
7 path::PathBuf,
8 rc::Rc,
9 str::FromStr,
10 sync::{
11 atomic::{AtomicBool, Ordering as AtomicOrdering},
12 mpsc, Arc,
13 },
14};
15
16use crate::{
17 dynamic_gui::DynamicUiHooks,
18 nwg_ext::{
19 list_view_enable_groups, list_view_item_get_group_id, list_view_item_set_group_id,
20 list_view_set_group_info, list_view_sort_rows, window_is_valid, window_placement,
21 ListViewGroupAlignment, ListViewGroupInfo, NumberSelect2, WindowPlacement,
22 },
23 settings::{
24 AutoStart, ConfigWindowInfo, QuickSwitchMenu, TrayClickAction, TrayIconType, UiSettings,
25 },
26 tray::{SystemTray, SystemTrayRef, TrayPlugin},
27 vd,
28 window_filter::{ExportedWindowFilters, FilterAction, IntegerRange, TextPattern, WindowFilter},
29 window_info::WindowInfo,
30};
31
32struct BackgroundThread {
33 rx: mpsc::Receiver<WindowInfo>,
34 handle: Option<std::thread::JoinHandle<()>>,
35 should_exit: Arc<AtomicBool>,
36}
37impl Drop for BackgroundThread {
38 fn drop(&mut self) {
39 self.should_exit.store(true, AtomicOrdering::Release);
40 let Some(handle) = self.handle.take() else {
41 return;
42 };
43 let res = handle.join();
44 if !std::thread::panicking() {
45 res.unwrap();
46 }
47 }
48}
49
50use nwg::stretch::{
52 geometry::{Rect, Size},
53 style::{AlignSelf, Dimension as D, FlexDirection},
54};
55const fn uniform_rect<D: Copy>(size: D) -> Rect<D> {
56 Rect {
57 start: size,
58 end: size,
59 top: size,
60 bottom: size,
61 }
62}
63const MARGIN: Rect<D> = uniform_rect(D::Points(5.0));
64const TAB_BACKGROUND: Option<[u8; 3]> = Some([255, 255, 255]);
65
66#[derive(nwd::NwgPartial, nwd::NwgUi, Default)]
67pub struct ConfigWindow {
68 tray: SystemTrayRef,
69
70 sidebar_layout: nwg::FlexboxLayout,
71 layout: nwg::FlexboxLayout,
72
73 tooltips: nwg::Tooltip,
74
75 #[nwg_control(
76 size: data.create_window_with_size(),
77 position: data.create_window_with_position(),
78 maximized: data.create_window_with_maximized(),
79 title: "Virtual Desktop Manager",
80 icon: crate::exe_icon().as_deref(),
81 )]
82 #[nwg_events(
83 OnWindowClose: [Self::on_close],
84 OnInit: [Self::on_init],
85 OnResizeEnd: [Self::on_resize_end],
86 OnMove: [Self::on_move],
87 OnMinMaxInfo: [Self::on_window_min_max_info(SELF, EVT_DATA)],
88 )]
89 pub window: nwg::Window,
90
91 #[nwg_control(
92 item_count: 10,
93 size: (500, 350),
94 list_style: nwg::ListViewStyle::Detailed,
95 focus: true,
96 ex_flags:
97 nwg::ListViewExFlags::GRID |
98 nwg::ListViewExFlags::FULL_ROW_SELECT |
99 nwg::ListViewExFlags::HEADER_DRAG_DROP,
100 )]
101 #[nwg_events(
103 OnListViewColumnClick: [Self::on_column_click(SELF, EVT_DATA)],
104 OnListViewItemActivated: [Self::on_list_view_item_activated(SELF, EVT_DATA)],
105 )]
106 data_view: nwg::ListView,
107 loaded_window_info: RefCell<Vec<WindowInfo>>,
108 loaded_filters: RefCell<Option<Arc<[WindowFilter]>>>,
109
110 #[nwg_control(parent: window)]
111 sidebar_tab_container: nwg::TabsContainer,
112
113 #[nwg_control(parent: sidebar_tab_container, text: "Filter options")]
114 filter_tab: nwg::Tab,
115
116 #[nwg_control(
117 parent: filter_tab, position: (5, 5), size: (230, 25),
118 text: "Selected filter index:",
119 background_color: TAB_BACKGROUND,
120 )]
121 filter_select_label: nwg::Label,
122
123 #[nwg_control(
124 parent: filter_tab, position: (5, 30), size: (225, 25),
125 min_int: 0, value_int: 0,
126 )]
127 #[nwg_events(OnNotice: [Self::on_select_filter_index_changed])]
128 filter_select_index: NumberSelect2,
129 selected_filter_index: Cell<Option<usize>>,
130
131 #[nwg_control(parent: filter_tab, position: (5, 60), size: (130, 25), text: "Create new filter")]
132 #[nwg_events(OnButtonClick: [Self::on_create_filter])]
133 filter_create_button: nwg::Button,
134
135 #[nwg_control(parent: filter_tab, position: (140, 60), size: (90, 25), text: "Delete filter")]
136 #[nwg_events(OnButtonClick: [Self::on_delete_current_filter])]
137 filter_delete_button: nwg::Button,
138
139 #[nwg_control(
140 parent: filter_tab, position: (5, 95), size: (230, 25),
141 text: "Window index:",
142 background_color: TAB_BACKGROUND,
143 )]
144 filter_window_index_label: nwg::Label,
145
146 #[nwg_control(
147 parent: filter_tab, position: (5, 115), size: (110, 25),
148 text: "Lower bound",
149 background_color: TAB_BACKGROUND,
150 )]
151 #[nwg_events(OnButtonClick: [Self::on_filter_config_ui_changed])]
152 filter_window_index_lower_checkbox: nwg::CheckBox,
153
154 #[nwg_control(
155 parent: filter_tab, position: (125, 115), size: (110, 25),
156 text: "Upper bound",
157 background_color: TAB_BACKGROUND,
158 )]
159 #[nwg_events(OnButtonClick: [Self::on_filter_config_ui_changed])]
160 filter_window_index_upper_checkbox: nwg::CheckBox,
161
162 #[nwg_control(
163 parent: filter_tab, position: (5, 140), size: (110, 25),
164 min_int: 1, value_int: 1,
165 )]
166 #[nwg_events(OnNotice: [Self::on_filter_config_ui_changed])]
167 filter_window_index_lower: NumberSelect2,
168
169 #[nwg_control(
170 parent: filter_tab, position: (125, 140), size: (110, 25),
171 min_int: 1, value_int: 1,
172 )]
173 #[nwg_events(OnNotice: [Self::on_filter_config_ui_changed])]
174 filter_window_index_upper: NumberSelect2,
175
176 #[nwg_control(
177 parent: filter_tab, position: (5, 175), size: (230, 25),
178 text: "Virtual desktop index:",
179 background_color: TAB_BACKGROUND,
180 )]
181 filter_desktop_index_label: nwg::Label,
182
183 #[nwg_control(
184 parent: filter_tab, position: (5, 195), size: (110, 25),
185 text: "Lower bound",
186 background_color: TAB_BACKGROUND,
187 )]
188 #[nwg_events(OnButtonClick: [Self::on_filter_config_ui_changed])]
189 filter_desktop_index_lower_checkbox: nwg::CheckBox,
190
191 #[nwg_control(
192 parent: filter_tab, position: (125, 195), size: (110, 25),
193 text: "Upper bound",
194 background_color: TAB_BACKGROUND,
195 )]
196 #[nwg_events(OnButtonClick: [Self::on_filter_config_ui_changed])]
197 filter_desktop_index_upper_checkbox: nwg::CheckBox,
198
199 #[nwg_control(
200 parent: filter_tab, position: (5, 225), size: (110, 25),
201 min_int: 1, value_int: 1,
202 )]
203 #[nwg_events(OnNotice: [Self::on_filter_config_ui_changed])]
204 filter_desktop_index_lower: NumberSelect2,
205
206 #[nwg_control(
207 parent: filter_tab, position: (125, 225), size: (110, 25),
208 min_int: 1, value_int: 1,
209 )]
210 #[nwg_events(OnNotice: [Self::on_filter_config_ui_changed])]
211 filter_desktop_index_upper: NumberSelect2,
212
213 #[nwg_control(
214 parent: filter_tab, position: (5, 260), size: (230, 25),
215 text: "Window title:",
216 background_color: TAB_BACKGROUND,
217 )]
218 filter_title_label: nwg::Label,
219
220 #[nwg_control(parent: filter_tab, position: (5, 285), size: (230, 85))]
221 #[nwg_events(OnTextInput: [Self::on_filter_config_ui_changed])]
222 filter_title: nwg::TextBox,
223
224 #[nwg_control(
225 parent: filter_tab, position: (5, 375), size: (230, 25),
226 text: "Process name:",
227 background_color: TAB_BACKGROUND,
228 )]
229 filter_process_label: nwg::Label,
230
231 #[nwg_control(parent: filter_tab, position: (5, 400), size: (230, 85))]
232 #[nwg_events(OnTextInput: [Self::on_filter_config_ui_changed])]
233 filter_process: nwg::TextBox,
234
235 #[nwg_control(
236 parent: filter_tab, position: (5, 495), size: (230, 25),
237 text: "Virtual desktop action to apply:",
238 background_color: TAB_BACKGROUND,
239 )]
240 filter_action_label: nwg::Label,
241
242 #[nwg_control(
243 parent: filter_tab, position: (5, 520), size: (230, 25),
244 collection: vec![FilterAction::Move, FilterAction::UnpinAndMove, FilterAction::Unpin, FilterAction::Pin, FilterAction::Nothing, FilterAction::Disabled],
245 selected_index: Some(5),
246 )]
247 #[nwg_events(OnComboxBoxSelection: [Self::on_filter_config_ui_changed])]
248 filter_action: nwg::ComboBox<FilterAction>,
249
250 #[nwg_control(
251 parent: filter_tab, position: (5, 555), size: (230, 25),
252 text: "Move to virtual desktop at index:",
253 background_color: TAB_BACKGROUND,
254 )]
255 filter_target_desktop_label: nwg::Label,
256
257 #[nwg_control(
258 parent: filter_tab, position: (5, 580), size: (225, 25),
259 min_int: 1, value_int: 1,
260 )]
261 #[nwg_events(OnNotice: [Self::on_filter_config_ui_changed])]
262 filter_target_desktop: NumberSelect2,
263
264 #[nwg_control(parent: sidebar_tab_container, text: "Program settings")]
265 settings_tab: nwg::Tab,
266
267 #[nwg_control(
268 parent: settings_tab, position: (5, 5), size: (240, 25),
269 text: "Start program with admin rights",
270 background_color: TAB_BACKGROUND,
271 )]
272 #[nwg_events(OnButtonClick: [Self::on_settings_ui_changed])]
273 settings_start_as_admin: nwg::CheckBox,
274
275 #[nwg_control(
276 parent: settings_tab, position: (5, 35), size: (240, 25),
277 text: "Auto start with Windows:",
278 background_color: TAB_BACKGROUND,
279 )]
280 settings_auto_start_label: nwg::Label,
281
282 #[nwg_control(
283 parent: settings_tab, position: (5, 60), size: (240, 25),
284 collection: AutoStart::ALL.to_vec(),
285 selected_index: Some(0),
286 )]
287 #[nwg_events(OnComboxBoxSelection: [Self::on_settings_ui_changed])]
288 settings_auto_start: nwg::ComboBox<AutoStart>,
289
290 #[nwg_control(
291 parent: settings_tab, position: (5, 95), size: (240, 25),
292 text: "Prevent flashing windows",
293 background_color: TAB_BACKGROUND,
294 )]
295 #[nwg_events(OnButtonClick: [Self::on_settings_ui_changed])]
296 settings_prevent_flashing_windows: nwg::CheckBox,
297
298 #[nwg_control(
299 parent: settings_tab, position: (5, 125), size: (240, 25),
300 text: "Smoothly switch virtual desktop",
301 background_color: TAB_BACKGROUND,
302 )]
303 #[nwg_events(OnButtonClick: [Self::on_settings_ui_changed])]
304 settings_smooth_switch_desktop: nwg::CheckBox,
305
306 #[nwg_control(
307 parent: settings_tab, position: (5, 155), size: (240, 25),
308 text: "Tray icon:",
309 background_color: TAB_BACKGROUND,
310 )]
311 settings_tray_icon_label: nwg::Label,
312
313 #[nwg_control(
314 parent: settings_tab, position: (5, 180), size: (240, 25),
315 collection: TrayIconType::ALL.to_vec(),
316 selected_index: Some(0),
317 )]
318 #[nwg_events(OnComboxBoxSelection: [Self::on_settings_ui_changed])]
319 settings_tray_icon: nwg::ComboBox<TrayIconType>,
320
321 #[nwg_control(
322 parent: settings_tab, position: (5, 215), size: (240, 25),
323 text: "Quick switch context menu:",
324 background_color: TAB_BACKGROUND,
325 )]
326 settings_quick_menu_label: nwg::Label,
327
328 #[nwg_control(
329 parent: settings_tab, position: (5, 240), size: (240, 25),
330 collection: QuickSwitchMenu::ALL.to_vec(),
331 selected_index: Some(0),
332 )]
333 #[nwg_events(OnComboxBoxSelection: [Self::on_settings_ui_changed])]
334 settings_quick_menu: nwg::ComboBox<QuickSwitchMenu>,
335
336 #[nwg_control(
337 parent: settings_tab, position: (5, 275), size: (240, 25),
338 text: "Quick switch menu shortcuts:",
339 background_color: TAB_BACKGROUND,
340 )]
341 settings_quick_menu_shortcuts_label: nwg::Label,
342
343 #[nwg_control(parent: settings_tab, position: (5, 300), size: (240, 85))]
344 #[nwg_events(OnTextInput: [Self::on_settings_ui_changed])]
345 settings_quick_menu_shortcuts: nwg::TextBox,
346
347 #[nwg_control(
348 parent: settings_tab, position: (5, 395), size: (240, 25),
349 text: "Quick shortcuts in submenus",
350 background_color: TAB_BACKGROUND,
351 )]
352 #[nwg_events(OnButtonClick: [Self::on_settings_ui_changed])]
353 settings_quick_menu_shortcuts_in_submenus: nwg::CheckBox,
354
355 #[nwg_control(
356 parent: settings_tab, position: (5, 430), size: (240, 25),
357 text: "Global hotkey for quick switch:",
358 background_color: TAB_BACKGROUND,
359 )]
360 settings_quick_menu_hotkey_label: nwg::Label,
361
362 #[nwg_control(parent: settings_tab, position: (5, 455), size: (240, 28))]
363 #[nwg_events(OnTextInput: [Self::on_settings_ui_changed])]
364 settings_quick_menu_hotkey: nwg::TextInput,
365
366 #[nwg_control(parent: settings_tab,
367 position: (5, 490), size: (240, 46),
368 readonly: true,
369 flags: "HSCROLL | AUTOHSCROLL | TAB_STOP | VISIBLE",
370 )]
371 settings_quick_menu_hotkey_error: nwg::TextBox,
372
373 #[nwg_control(
374 parent: settings_tab, position: (5, 550), size: (240, 25),
375 text: "Left click on tray icon:",
376 background_color: TAB_BACKGROUND,
377 )]
378 settings_left_click_label: nwg::Label,
379
380 #[nwg_control(
381 parent: settings_tab, position: (5, 575), size: (240, 25),
382 collection: TrayClickAction::ALL.to_vec(),
383 selected_index: Some(0),
384 )]
385 #[nwg_events(OnComboxBoxSelection: [Self::on_settings_ui_changed])]
386 settings_left_click: nwg::ComboBox<TrayClickAction>,
387
388 #[nwg_control(
389 parent: settings_tab, position: (5, 610), size: (240, 25),
390 text: "Middle click on tray icon:",
391 background_color: TAB_BACKGROUND,
392 )]
393 settings_middle_click_label: nwg::Label,
394
395 #[nwg_control(
396 parent: settings_tab, position: (5, 635), size: (240, 25),
397 collection: TrayClickAction::ALL.to_vec(),
398 selected_index: Some(0),
399 )]
400 #[nwg_events(OnComboxBoxSelection: [Self::on_settings_ui_changed])]
401 settings_middle_click: nwg::ComboBox<TrayClickAction>,
402
403 #[nwg_control(
404 parent: settings_tab, position: (5, 680), size: (240, 40),
405 text: "Global hotkey to open context\r\nmenu at current mouse position:",
406 background_color: TAB_BACKGROUND,
407 )]
408 settings_open_menu_at_mouse_pos_hotkey_label: nwg::Label,
409
410 #[nwg_control(parent: settings_tab, position: (5, 680 + 50), size: (240, 28))]
411 #[nwg_events(OnTextInput: [Self::on_settings_ui_changed])]
412 settings_open_menu_at_mouse_pos_hotkey: nwg::TextInput,
413
414 #[nwg_control(parent: settings_tab,
415 position: (5, 680 + 50 + 35), size: (240, 46),
416 readonly: true,
417 flags: "HSCROLL | AUTOHSCROLL | TAB_STOP | VISIBLE",
418 )]
419 settings_open_menu_at_mouse_pos_hotkey_error: nwg::TextBox,
420
421 #[nwg_control(parent: window, flags: "VISIBLE")]
422 utils_frame: nwg::Frame,
423
424 #[nwg_control(parent: utils_frame, position: (0, 5), size: (125, 30), text: "Import filters")]
425 #[nwg_events(OnButtonClick: [Self::on_import_filters])]
426 utils_import: nwg::Button,
427
428 #[nwg_control(parent: utils_frame, position: (130, 5), size: (130, 30), text: "Export filters")]
429 #[nwg_events(OnButtonClick: [Self::on_export_filters])]
430 utils_export: nwg::Button,
431
432 #[nwg_control(parent: utils_frame, position: (0, 45), size: (100, 55), text: "Refresh info")]
433 #[nwg_events(OnButtonClick: [Self::on_refresh_info])]
434 utils_refresh: nwg::Button,
435
436 #[nwg_control(parent: utils_frame, position: (105, 45), size: (155, 55), text: "Apply filters")]
437 #[nwg_events(OnButtonClick: [Self::on_apply_filters])]
438 utils_apply_filters: nwg::Button,
439
440 background_thread: RefCell<Option<BackgroundThread>>,
441 has_queued_refresh: Cell<bool>,
442 is_data_sorted: Cell<bool>,
443
444 #[nwg_control(parent: window)]
445 #[nwg_events(OnNotice: [Self::on_data])]
446 data_notice: nwg::Notice,
447
448 is_closed: Cell<bool>,
449 pub open_soon: Cell<bool>,
450
451 export_dialog: OnceCell<nwg::FileDialog>,
452 import_dialog: OnceCell<nwg::FileDialog>,
453}
454impl ConfigWindow {
456 const GROUP_WINDOWS: i32 = 1;
457 const GROUP_FILTERS: i32 = 2;
458
459 const COLUMN_WINDOWS_INDEX: usize = 0;
460 const COLUMN_FILTERS_INDEX: usize = 4;
461 const COLUMN_TARGET_DESKTOP: usize = 5;
462
463 fn create_window_with_size(&self) -> (i32, i32) {
464 let (x, y) = self
465 .tray
466 .get()
467 .map(|tray| tray.settings().get().config_window)
468 .unwrap_or_default()
469 .size;
470 let (min_x, min_y) = Self::MIN_SIZE;
471 ((x as i32).max(min_x), (y as i32).max(min_y))
472 }
473 fn create_window_with_position(&self) -> (i32, i32) {
474 self.tray
475 .get()
476 .and_then(|tray| tray.settings().get().config_window.position)
477 .unwrap_or((300, 300))
478 }
479 fn create_window_with_maximized(&self) -> bool {
480 self.tray
481 .get()
482 .map(|tray| tray.settings().get().config_window)
483 .unwrap_or_default()
484 .maximized
485 }
486
487 fn build_layout(&self) -> Result<(), nwg::NwgError> {
488 let ui = self;
489
490 let mut sidebar_layout = nwg::FlexboxLayout::builder()
492 .parent(&ui.window)
493 .flex_direction(FlexDirection::Column);
494 sidebar_layout = sidebar_layout
496 .child(&ui.sidebar_tab_container)
497 .child_margin(MARGIN)
498 .child_align_self(AlignSelf::Stretch)
499 .child_flex_grow(1.0)
500 .child_size(Size {
501 width: D::Points(260.0),
502 height: D::Auto,
503 });
504 sidebar_layout = sidebar_layout
506 .child(&ui.utils_frame)
507 .child_margin(MARGIN)
508 .child_align_self(AlignSelf::Stretch)
509 .child_size(Size {
510 width: D::Points(260.0),
511 height: D::Points(100.0),
512 });
513 sidebar_layout.build_partial(&ui.sidebar_layout)?;
515
516 let mut main_layout = nwg::FlexboxLayout::builder()
518 .parent(&ui.window)
519 .flex_direction(FlexDirection::Row)
520 .padding(uniform_rect(D::Points(5.0)));
521 main_layout = main_layout
523 .child(&ui.data_view)
524 .child_margin(MARGIN)
525 .child_flex_grow(1.0)
526 .child_size(Size {
527 width: D::Auto,
528 height: D::Auto,
529 });
530 main_layout = main_layout
532 .child_layout(&ui.sidebar_layout)
533 .child_size(Size {
534 width: D::Points(270.0),
535 height: D::Auto,
536 })
537 .child_align_self(AlignSelf::Stretch);
538 main_layout.build(&ui.layout)?;
539 Ok(())
540 }
541 fn build_tooltip(&mut self) -> Result<(), nwg::NwgError> {
542 nwg::Tooltip::builder()
543 .register(
544 self.settings_start_as_admin.handle,
545 "This is useful in order to move windows owned by other \
546 programs that have admin rights.",
547 )
548 .register(
549 self.settings_prevent_flashing_windows.handle,
550 "Some windows can try to grab attention by flashing their \
551 icon in the taskbar, this option suppresses such flashing right \
552 after window filters are applied.",
553 )
554 .register(
555 self.settings_smooth_switch_desktop.handle,
556 "Enable for this program to use animations when changing \
557 the current virtual desktop.",
558 )
559 .register(
560 self.utils_import.handle,
561 "Add new filters by loading them from a selected file.",
562 )
563 .register(self.utils_export.handle, "Save all filters to a file")
564 .register(
565 self.utils_refresh.handle,
566 "Reload info about all open windows",
567 )
568 .register(
569 self.utils_apply_filters.handle,
570 "Use the configured filters to move windows to specific virtual desktops",
571 )
572 .register(
573 &self.settings_quick_menu_shortcuts_label,
574 "Each line should have a letter or symbol followed by a zero-based \
575 virtual desktop index. For each line an extra context menu item will \
576 be created in the quick switch menu with that symbol as its access key.",
577 )
578 .register(
579 &self.settings_quick_menu_shortcuts_in_submenus,
580 "If checked then extra context menu items for quick switch shortcuts \
581 will be created in each submenu of the quick switch menu when there are \
582 more than 9 virtual desktops.",
583 )
584 .register(
585 &self.settings_middle_click_label,
586 "Controls the action that will be preformed when the tray icon \
587 is middle clicked. On some Windows 11 versions middle clicks are \
588 registered as left clicks.",
589 )
590 .build(&mut self.tooltips)?;
591 Ok(())
592 }
593
594 fn on_init(&self) {
595 let dv = &self.data_view;
596
597 dv.set_headers_enabled(true);
598
599 debug_assert_eq!(Self::COLUMN_WINDOWS_INDEX, dv.column_len());
600 dv.insert_column(nwg::InsertListViewColumn {
601 index: Some(dv.column_len() as _),
602 fmt: Some(nwg::ListViewColumnFlags::LEFT),
603 width: Some(100),
604 text: Some("Window Index".into()),
605 });
606
607 dv.insert_column(nwg::InsertListViewColumn {
608 index: Some(dv.column_len() as _),
609 fmt: Some(nwg::ListViewColumnFlags::LEFT),
610 width: Some(100),
611 text: Some("Virtual Desktop".into()),
612 });
613
614 dv.insert_column(nwg::InsertListViewColumn {
615 index: Some(dv.column_len() as _),
616 fmt: Some(nwg::ListViewColumnFlags::LEFT),
617 width: Some(200),
618 text: Some("Window Title".into()),
619 });
620
621 dv.insert_column(nwg::InsertListViewColumn {
622 index: Some(dv.column_len() as _),
623 fmt: Some(nwg::ListViewColumnFlags::LEFT),
624 width: Some(200),
625 text: Some("Process Name".into()),
626 });
627
628 debug_assert_eq!(Self::COLUMN_FILTERS_INDEX, dv.column_len());
629 dv.insert_column(nwg::InsertListViewColumn {
630 index: Some(dv.column_len() as _),
631 fmt: Some(nwg::ListViewColumnFlags::LEFT),
632 width: Some(100),
633 text: Some("Filter Index".into()),
634 });
635
636 debug_assert_eq!(Self::COLUMN_TARGET_DESKTOP, dv.column_len());
637 dv.insert_column(nwg::InsertListViewColumn {
638 index: Some(dv.column_len() as _),
639 fmt: Some(nwg::ListViewColumnFlags::LEFT),
640 width: Some(100),
641 text: Some("Target Desktop".into()),
642 });
643
644 dv.set_column_sort_arrow(0, None);
645
646 list_view_enable_groups(dv, true);
647 list_view_set_group_info(
648 dv,
649 ListViewGroupInfo {
650 create_new: true,
651 group_id: Self::GROUP_WINDOWS,
652 header: Some("Active Windows".into()),
653 header_alignment: Some(ListViewGroupAlignment::Left),
654 ..Default::default()
655 },
656 );
657 list_view_set_group_info(
658 dv,
659 ListViewGroupInfo {
660 create_new: true,
661 group_id: Self::GROUP_FILTERS,
662 header: Some("Filters / Rules".into()),
663 header_alignment: Some(ListViewGroupAlignment::Left),
664 ..Default::default()
665 },
666 );
667
668 self.sync_filter_from_settings(None);
669 self.set_selected_filter_index(Some(0));
670 self.gather_window_info();
671 }
672}
673impl ConfigWindow {
675 fn on_column_click(&self, data: &nwg::EventData) {
676 let &nwg::EventData::OnListViewItemIndex { column_index, .. } = data else {
677 tracing::error!(event_data = ?data, "ConfigWindow::on_column_click: got unexpected event data");
678 return;
679 };
680 tracing::trace!(event_data = ?data, "ConfigWindow::on_column_click");
681
682 let sort_dir = self.data_view.column_sort_arrow(column_index);
683 let new_sort_dir = match sort_dir {
684 Some(nwg::ListViewColumnSortArrow::Up)
685 if column_index == Self::COLUMN_WINDOWS_INDEX =>
686 {
687 None
688 }
689 Some(nwg::ListViewColumnSortArrow::Up) => Some(nwg::ListViewColumnSortArrow::Down),
690 Some(nwg::ListViewColumnSortArrow::Down) => Some(nwg::ListViewColumnSortArrow::Up),
691 None => Some(nwg::ListViewColumnSortArrow::Down),
692 };
693 tracing::debug!(column_index, ?sort_dir, ?new_sort_dir, "on_column_click");
694 self.data_view
695 .set_column_sort_arrow(column_index, new_sort_dir);
696 for i in 0..self.data_view.column_len() {
697 if i == column_index {
698 continue;
699 }
700 self.data_view.set_column_sort_arrow(i, None);
701 }
702 self.sort_items(
703 Some(column_index).filter(|_| new_sort_dir.is_some()),
704 new_sort_dir,
705 );
706 }
707 fn get_sort_info(&self) -> (Option<usize>, Option<nwg::ListViewColumnSortArrow>) {
708 for i in 0..self.data_view.column_len() {
709 let sort_dir = self.data_view.column_sort_arrow(i);
710 if sort_dir.is_some() {
711 return (Some(i), sort_dir);
712 }
713 }
714 (None, None)
715 }
716 fn resort_items(&self) {
717 let (index, sort_dir) = self.get_sort_info();
718 self.sort_items(index, sort_dir);
719 }
720 fn sort_items(
721 &self,
722 column_index: Option<usize>,
723 sort_dir: Option<nwg::ListViewColumnSortArrow>,
724 ) {
725 list_view_sort_rows(&self.data_view, |a_ix, b_ix| {
726 let a_group = list_view_item_get_group_id(&self.data_view, a_ix);
727 let b_group = list_view_item_get_group_id(&self.data_view, b_ix);
728 let group_cmp = a_group.cmp(&b_group);
729 if group_cmp.is_ne() {
730 return group_cmp;
732 }
733
734 let mut using_fallback = column_index.is_none();
735 let result = loop {
736 let current_column_index = if using_fallback {
737 if a_group == Self::GROUP_FILTERS {
738 Self::COLUMN_FILTERS_INDEX
739 } else if a_group == Self::GROUP_WINDOWS {
740 Self::COLUMN_WINDOWS_INDEX
741 } else {
742 tracing::warn!("Tried to sort row that was neither a window or a filter");
743 column_index.unwrap_or_default()
744 }
745 } else {
746 column_index.unwrap_or_default()
747 };
748 let a = self.data_view.item(a_ix, current_column_index, 4096);
749 let b = self.data_view.item(b_ix, current_column_index, 4096);
750 let (a, b) = match (a, b) {
751 (Some(a), Some(b)) => (a, b),
752 (None, Some(_)) => {
753 tracing::warn!("Failed to get list item at row {}", a_ix);
754 return Ordering::Greater;
756 }
757 (Some(_), None) => {
758 tracing::warn!("Failed to get list item at row {}", b_ix);
759 return Ordering::Less;
760 }
761 (None, None) => {
762 tracing::warn!("Failed to get list item at row {} and row {}", a_ix, b_ix);
763 return Ordering::Equal;
764 }
765 };
766
767 let result = match a
768 .text
769 .parse::<i64>()
770 .and_then(|a| Ok((a, b.text.parse::<i64>()?)))
771 {
772 Ok((a, b)) => a.cmp(&b),
773 Err(_) => a.text.cmp(&b.text),
774 };
775 if result.is_ne() || using_fallback {
776 break result;
777 } else {
778 using_fallback = true;
779 }
780 };
781 if using_fallback {
782 result
783 } else if let Some(nwg::ListViewColumnSortArrow::Up) = sort_dir {
784 result.reverse()
785 } else {
786 result
787 }
788 });
789 self.is_data_sorted.set(true);
790 }
791}
792impl ConfigWindow {
794 fn clear_window_info(&self) {
795 for ix in (0..self.data_view.len()).rev() {
796 let group = list_view_item_get_group_id(&self.data_view, ix);
797 if group == Self::GROUP_WINDOWS {
798 self.data_view.remove_item(ix);
799 }
800 }
801 self.loaded_window_info.replace(Vec::new());
802 }
803 fn determine_active_filter_indexes_for_window(
804 &self,
805 window_index: i32,
806 window: &WindowInfo,
807 ) -> String {
808 self.loaded_filters
809 .borrow()
810 .as_deref()
811 .unwrap_or_default()
812 .iter()
813 .enumerate()
814 .filter(|(_, rule)| rule.check_window(window_index, window))
816 .map(|(ix, _)| (ix + 1).to_string())
818 .collect::<Vec<_>>()
819 .join(", ")
820 }
821 fn add_window_info(&self, window: WindowInfo) {
822 let index = {
823 let mut guard = self.loaded_window_info.borrow_mut();
824 let index = guard.len();
825 guard.push(window.clone());
826 index
827 };
828
829 let filter_indexes = self.determine_active_filter_indexes_for_window(index as i32, &window);
830 let action = WindowFilter::find_first_action(
831 self.loaded_filters.borrow().as_deref().unwrap_or_default(),
832 index as i32,
833 &window,
834 )
835 .map(|filter| filter.display_target_desktop().to_string());
836
837 let WindowInfo {
838 handle: _,
839 title,
840 process_id: _,
841 process_name,
842 virtual_desktop,
843 } = window;
844
845 let virtual_desktop = format!("{virtual_desktop}");
846 let one_based_index = (index + 1).to_string();
847 let info = [
848 one_based_index.as_str(),
849 virtual_desktop.as_str(),
850 title.as_str(),
851 &*process_name,
852 filter_indexes.as_str(),
853 action.as_deref().unwrap_or_default(),
854 ];
855 self.data_view.insert_items_row(None, &info);
856 list_view_item_set_group_id(
857 &self.data_view,
858 self.data_view.len().saturating_sub(1),
859 Some(Self::GROUP_WINDOWS),
860 );
861 self.is_data_sorted.set(false);
862 }
863 fn update_window_infos(&self) {
864 for row_ix in (0..self.data_view.len()).rev() {
865 let group = list_view_item_get_group_id(&self.data_view, row_ix);
866 if group != Self::GROUP_WINDOWS {
867 continue;
868 }
869 let Some(window_index_item) =
870 self.data_view.item(row_ix, Self::COLUMN_WINDOWS_INDEX, 10)
871 else {
872 continue;
873 };
874
875 let Ok(window_index) = window_index_item.text.parse::<usize>() else {
876 continue;
877 };
878 let window_index = window_index - 1;
880 let Some(window_info) = self.loaded_window_info.borrow().get(window_index).cloned()
881 else {
882 continue;
883 };
884
885 let filter_indexes =
886 self.determine_active_filter_indexes_for_window(window_index as i32, &window_info);
887 self.data_view.update_item(
888 row_ix,
889 nwg::InsertListViewItem {
890 index: Some(row_ix as _),
891 column_index: Self::COLUMN_FILTERS_INDEX as _,
892 text: Some(filter_indexes),
893 image: None,
894 },
895 );
896
897 let action = WindowFilter::find_first_action(
898 self.loaded_filters.borrow().as_deref().unwrap_or_default(),
899 window_index as i32,
900 &window_info,
901 )
902 .map(|filter| filter.display_target_desktop().to_string());
903 self.data_view.update_item(
904 row_ix,
905 nwg::InsertListViewItem {
906 index: Some(row_ix as _),
907 column_index: Self::COLUMN_TARGET_DESKTOP as _,
908 text: Some(action.unwrap_or_default()),
909 image: None,
910 },
911 );
912 }
913 }
914
915 fn gather_window_info(&self) {
916 let mut guard = self.background_thread.borrow_mut();
917 if matches!(
918 &*guard,
919 Some(BackgroundThread { handle: Some(handle), should_exit, .. })
920 if !handle.is_finished() && !should_exit.load(AtomicOrdering::Acquire)
921 ) {
922 self.has_queued_refresh.set(true);
923 return; }
925 self.clear_window_info();
926 self.has_queued_refresh.set(false);
927
928 let (tx, rx) = mpsc::channel();
929 let notice_tx = self.data_notice.sender();
930 let should_exit = <Arc<AtomicBool>>::default();
931 let handle = std::thread::Builder::new()
932 .name("ConfigWindowBackgroundThread".to_owned())
933 .spawn({
934 let should_exit = Arc::clone(&should_exit);
935 move || {
936 if vd::has_loaded_dynamic_library_successfully() {
937 if let Err(e) = unsafe { windows::Win32::System::Com::CoInitialize(None) }.ok() {
940 tracing::warn!(
941 error = e.to_string(),
942 "Failed to call CoInitialize on ConfigWindowBackgroundThread"
943 );
944 }
945 }
946 for result in WindowInfo::try_get_all() {
947 if let Ok(window) = result {
948 tracing::trace!(info = ?window, "Sending window info to config window");
949 if tx.send(window).is_err() {
950 tracing::debug!("Canceled config window background thread since receiver was closed");
951 return;
952 }
953 notice_tx.notice();
954 }
955 if should_exit.load(AtomicOrdering::Relaxed) {
956 tracing::debug!(
957 "Canceled config window background thread since it was requested"
958 );
959 return;
960 }
961 }
962 should_exit.store(true, AtomicOrdering::Relaxed);
963 drop(tx);
965 notice_tx.notice();
966 tracing::debug!(
967 "Config window background thread has gathered info about all windows"
968 );
969 }
970 }).expect("Failed to spawn config window thread");
971
972 *guard = Some(BackgroundThread {
974 rx,
975 handle: Some(handle),
976 should_exit,
977 });
978 }
979 fn on_data(&self) {
980 let Ok(guard) = self.background_thread.try_borrow() else {
981 tracing::warn!("Received notice from background thread while RefCell was locked, might delay a table update");
982 return;
983 };
984 let Some(background) = &*guard else {
985 tracing::warn!(
986 "Received notice from background thread, but no such thread was running"
987 );
988 return;
989 };
990 tracing::trace!("ConfigWindow::on_data");
991 loop {
992 match background.rx.try_recv() {
993 Ok(window) => {
994 tracing::trace!(info = ?window, "Received window info from background thread");
995 self.add_window_info(window);
996 continue;
997 }
998 Err(mpsc::TryRecvError::Disconnected) => {
999 drop(guard);
1001 self.on_gathered_all_window_info();
1002 }
1003 Err(mpsc::TryRecvError::Empty) => {
1004 }
1006 }
1007 break;
1008 }
1009 }
1010 fn on_gathered_all_window_info(&self) {
1011 if !self.is_data_sorted.get() {
1012 self.resort_items();
1013 }
1014 if self.has_queued_refresh.get() {
1015 self.gather_window_info();
1016 }
1017 }
1018}
1019impl ConfigWindow {
1021 const MIN_SIZE: (i32, i32) = (300, 1025);
1022
1023 pub fn is_closed(&self) -> bool {
1024 self.is_closed.get() || !window_is_valid(self.window.handle)
1025 }
1026 pub fn set_as_foreground_window(&self) {
1027 let Some(handle) = self.window.handle.hwnd() else {
1028 return;
1029 };
1030 unsafe {
1031 let _ = windows::Win32::UI::WindowsAndMessaging::SetForegroundWindow(
1032 windows::Win32::Foundation::HWND(handle.cast()),
1033 );
1034 }
1035 }
1036
1037 fn save_position_and_size(&self) {
1038 let Some(tray) = self.tray.get() else {
1039 return;
1040 };
1041 let pos = self.window.position();
1042 let size = self.window.size();
1043 let placement = window_placement(&self.window).unwrap_or(WindowPlacement::Minimized);
1044 let maximized = placement == WindowPlacement::Maximized;
1045
1046 tracing::trace!(
1047 position =? pos,
1048 size =? size,
1049 ?placement,
1050 "Config window resized or moved"
1051 );
1052 if placement == WindowPlacement::Minimized {
1053 return;
1054 }
1055
1056 tray.settings().update(|prev| UiSettings {
1057 config_window: if maximized {
1058 ConfigWindowInfo {
1060 maximized,
1061 ..prev.config_window
1062 }
1063 } else {
1064 ConfigWindowInfo {
1065 position: Some(pos),
1066 size,
1067 maximized,
1068 }
1069 },
1070 ..prev.clone()
1071 });
1072 }
1073 fn on_resize_end(&self) {
1074 self.save_position_and_size();
1075 }
1076 fn on_move(&self) {
1077 self.save_position_and_size();
1078 }
1079 fn on_close(&self) {
1080 self.is_closed.set(true);
1081 if let Some(background) = &*self.background_thread.borrow() {
1082 background.should_exit.store(true, AtomicOrdering::Release);
1083 }
1084 }
1085 fn on_window_min_max_info(&self, data: &nwg::EventData) {
1086 let nwg::EventData::OnMinMaxInfo(info) = data else {
1087 return;
1088 };
1089 let (width, height) = Self::MIN_SIZE;
1090 info.set_min_size(width, height);
1091 }
1092}
1093impl ConfigWindow {
1095 fn on_apply_filters(&self) {
1096 let Some(tray) = self.tray.get() else {
1097 return;
1098 };
1099 tray.apply_filters();
1100 }
1101 fn on_refresh_info(&self) {
1102 self.gather_window_info();
1103 }
1104 fn on_export_filters(&self) {
1105 let dialog = if let Some(dialog) = self.export_dialog.get() {
1106 dialog
1107 } else {
1108 let mut dialog = nwg::FileDialog::default();
1109 if let Err(e) = nwg::FileDialog::builder()
1110 .title("Export Virtual Desktop Manager Rules / Filters")
1111 .action(nwg::FileDialogAction::Save)
1112 .filters("JSON filters(*.json)|Xml legacy filters(*.xml;*.txt)|Any filter file(*.json;*.xml;*.txt)|All files(*)")
1113 .build(&mut dialog)
1114 {
1115 tracing::error!(error = e.to_string(), "Failed to create export dialog");
1116 return;
1117 }
1118 self.export_dialog.get_or_init(|| dialog)
1119 };
1120 if !dialog.run(Some(self.window.handle)) {
1121 return;
1122 }
1123 let Ok(mut selected) = dialog
1124 .get_selected_item()
1125 .map(PathBuf::from)
1126 .inspect_err(|e| {
1127 tracing::error!(
1128 error = e.to_string(),
1129 "Failed to get selected item from export dialog"
1130 );
1131 })
1132 else {
1133 return;
1134 };
1135
1136 let mut allow_overwrite = true;
1138
1139 let is_legacy = if let Some(ext) = selected.extension() {
1140 ext.eq_ignore_ascii_case("xml") || ext.eq_ignore_ascii_case("txt")
1141 } else {
1142 selected.set_extension("json");
1143 allow_overwrite = false; false
1145 };
1146 let Some(data) = (if is_legacy {
1147 #[cfg(feature = "persist_filters_xml")]
1148 {
1149 let filters = self.loaded_filters.borrow().clone();
1150 WindowFilter::serialize_to_xml(filters.as_deref().unwrap_or_default())
1151 .inspect_err(|e| {
1152 nwg::error_message(
1153 "Virtual Desktop Manager - Export error",
1154 &format!("Failed to convert filters to legacy XML format:\n{e}"),
1155 );
1156 })
1157 .ok()
1158 }
1159 #[cfg(not(feature = "persist_filters_xml"))]
1160 {
1161 nwg::error_message(
1162 "Virtual Desktop Manager - Export error",
1163 "This program was compiled without support for legacy XML filters/rules. \
1164 Recompile the program from source with the \"persist_filters_xml\" feature \
1165 in order to support exporting such filter files.",
1166 );
1167 None
1168 }
1169 } else {
1170 #[cfg(feature = "persist_filters")]
1171 {
1172 let exported = ExportedWindowFilters {
1173 filters: self
1174 .loaded_filters
1175 .borrow()
1176 .clone()
1177 .unwrap_or_default()
1178 .to_vec(),
1179 ..Default::default()
1180 };
1181 serde_json::to_string_pretty(&exported)
1182 .inspect_err(|e| {
1183 nwg::error_message(
1184 "Virtual Desktop Manager - Export error",
1185 &format!("Failed to convert filters to JSON:\n{e}"),
1186 );
1187 })
1188 .ok()
1189 }
1190 #[cfg(not(feature = "persist_filters"))]
1191 {
1192 nwg::error_message(
1193 "Virtual Desktop Manager - Export error",
1194 "This program was compiled without support for JSON filters/rules. \
1195 Recompile the program from source with the \"persist_filters\" feature \
1196 in order to support exporting such filter files.",
1197 );
1198 None
1199 }
1200 }) else {
1201 return;
1202 };
1203 let Ok(mut file) = OpenOptions::new()
1204 .create(true)
1205 .create_new(!allow_overwrite)
1206 .write(true)
1207 .truncate(true)
1208 .open(selected.as_path())
1209 .inspect_err(|e| {
1210 nwg::error_message(
1211 "Virtual Desktop Manager - Export error",
1212 &format!("Failed to create file at \"{}\":\n{e}", selected.display()),
1213 );
1214 })
1215 else {
1216 return;
1217 };
1218 if let Err(e) = file.write_all(data.as_bytes()) {
1219 nwg::error_message(
1220 "Virtual Desktop Manager - Export error",
1221 &format!(
1222 "Failed to write data to file at \"{}\":\n{e}",
1223 selected.display()
1224 ),
1225 );
1226 }
1227 }
1228 fn on_import_filters(&self) {
1229 let dialog = if let Some(dialog) = self.import_dialog.get() {
1230 dialog
1231 } else {
1232 let mut dialog = nwg::FileDialog::default();
1233 if let Err(e) = nwg::FileDialog::builder()
1234 .title("Import Virtual Desktop Manager Rules / Filters")
1235 .action(nwg::FileDialogAction::Open)
1236 .filters("Any filter file(*.json;*.xml;*.txt)|JSON filters(*.json)|Xml legacy filters(*.xml;*.txt)|All files(*)")
1237 .build(&mut dialog)
1238 {
1239 tracing::error!(error = e.to_string(), "Failed to create import dialog");
1240 return;
1241 }
1242 self.import_dialog.get_or_init(|| dialog)
1243 };
1244 if !dialog.run(Some(self.window.handle)) {
1245 return;
1246 }
1247 let Ok(selected) = dialog
1248 .get_selected_item()
1249 .map(PathBuf::from)
1250 .inspect_err(|e| {
1251 tracing::error!(
1252 error = e.to_string(),
1253 "Failed to get selected item from import dialog"
1254 );
1255 })
1256 else {
1257 return;
1258 };
1259 let data = match std::fs::read_to_string(selected.as_path()) {
1260 Ok(v) => v,
1261 Err(e) => {
1262 nwg::error_message(
1263 "Virtual Desktop Manager - Import error",
1264 &format!(
1265 "Error when reading file with filter/rule at \"{}\":\n\n{e}",
1266 selected.display()
1267 ),
1268 );
1269 return;
1270 }
1271 };
1272 let is_legacy = selected
1273 .extension()
1274 .is_some_and(|ext| ext.eq_ignore_ascii_case("xml") || ext.eq_ignore_ascii_case("txt"));
1275
1276 let Some(imported) = (if is_legacy {
1277 #[cfg(feature = "persist_filters_xml")]
1278 {
1279 WindowFilter::deserialize_from_xml(&data)
1280 .inspect_err(|e| {
1281 nwg::error_message(
1282 "Virtual Desktop Manager - Import error",
1283 &format!("Failed to parse legacy XML filters/rules:\n{e}"),
1284 );
1285 })
1286 .ok()
1287 }
1288 #[cfg(not(feature = "persist_filters_xml"))]
1289 {
1290 nwg::error_message(
1291 "Virtual Desktop Manager - Import error",
1292 "This program was compiled without support for legacy XML filters/rules. \
1293 Recompile the program from source with the \"persist_filters_xml\" feature \
1294 in order to support such filter files.",
1295 );
1296 None
1297 }
1298 } else {
1299 #[cfg(feature = "persist_filters")]
1300 {
1301 let mut deserializer = serde_json::Deserializer::from_str(&data);
1302 let result: Result<ExportedWindowFilters, _> = {
1303 #[cfg(not(feature = "serde_path_to_error"))]
1304 {
1305 serde::Deserialize::deserialize(&mut deserializer)
1306 }
1307 #[cfg(feature = "serde_path_to_error")]
1308 {
1309 serde_path_to_error::deserialize(&mut deserializer)
1310 }
1311 };
1312 result
1313 .inspect_err(|e| {
1314 nwg::error_message(
1315 "Virtual Desktop Manager - Import error",
1316 &format!("Failed to parse JSON filters/rules:\n{e}"),
1317 );
1318 })
1319 .ok()
1320 .map(|info| info.migrate_and_get_filters())
1321 }
1322 #[cfg(not(feature = "persist_filters"))]
1323 {
1324 nwg::error_message(
1325 "Virtual Desktop Manager - Import error",
1326 "This program was compiled without support for JSON filters/rules. \
1327 Recompile the program from source with the \"persist_filters\" feature \
1328 in order to support such filter files.",
1329 );
1330 None
1331 }
1332 }) else {
1333 return;
1334 };
1335 let Some(tray) = self.tray.get() else {
1336 return;
1337 };
1338 tray.settings().update(|prev| UiSettings {
1339 filters: prev.filters.iter().cloned().chain(imported).collect(),
1340 ..prev.clone()
1341 });
1342 }
1343}
1344impl ConfigWindow {
1346 fn on_list_view_item_activated(&self, data: &nwg::EventData) {
1347 tracing::debug!(?data, "ConfigWindow::on_list_view_item_activated");
1348 let &nwg::EventData::OnListViewItemIndex { row_index, .. } = data else {
1349 return;
1350 };
1351 let group = list_view_item_get_group_id(&self.data_view, row_index);
1352 if group != Self::GROUP_FILTERS {
1353 return;
1354 }
1355 let Some(filter_index_item) =
1356 self.data_view
1357 .item(row_index, Self::COLUMN_FILTERS_INDEX, 10)
1358 else {
1359 return;
1360 };
1361 let Ok(filter_index) = filter_index_item.text.parse::<usize>() else {
1362 return;
1363 };
1364
1365 self.set_selected_filter_index(Some(filter_index - 1));
1366 }
1367 fn on_select_filter_index_changed(&self) {
1368 let wanted = self.get_selected_filter_index();
1369 if self.selected_filter_index.get() != wanted {
1370 self.set_selected_filter_index(wanted);
1371 }
1372 }
1373 fn highlight_selected_filter_in_list(&self) {
1374 let selected = self.get_selected_filter_index();
1375 for row_index in 0..self.data_view.len() {
1376 let group = list_view_item_get_group_id(&self.data_view, row_index);
1377 if group != Self::GROUP_FILTERS {
1378 continue;
1379 }
1380 if let Some(filter_ix) = self
1381 .data_view
1382 .item(row_index, Self::COLUMN_FILTERS_INDEX, 10)
1383 {
1384 if let Ok(filter_ix) = filter_ix.text.parse::<usize>() {
1385 let filter_ix = filter_ix - 1;
1387 if Some(filter_ix) == selected {
1388 self.data_view.select_item(row_index, true);
1389 continue;
1390 }
1391 }
1392 }
1393 self.data_view.select_item(row_index, false);
1394 }
1395 }
1396 fn set_selected_filter_index(&self, index: Option<usize>) {
1397 self.selected_filter_index.set(index);
1398 let loaded_filters_len = self
1399 .loaded_filters
1400 .borrow()
1401 .as_deref()
1402 .unwrap_or_default()
1403 .len() as i64;
1404 self.filter_select_index
1405 .set_data(nwg::NumberSelectData::Int {
1406 value: index.map(|v| v + 1).unwrap_or(0) as i64,
1407 step: 1,
1408 max: loaded_filters_len,
1409 min: 0,
1411 });
1412 self.highlight_selected_filter_in_list();
1413 self.set_filter_config_enabled(index.is_some());
1414 if let Some(index) = index {
1415 let loaded_filters = self.loaded_filters.borrow().clone();
1416 let loaded_filters = loaded_filters.as_deref().unwrap_or_default();
1417 let Some(filter) = loaded_filters.get(index) else {
1418 self.set_selected_filter_index(None);
1419 return;
1420 };
1421 self.set_filter_config_for_sidebar(filter);
1422 } else {
1423 self.set_filter_config_for_sidebar(&Default::default());
1424 }
1425 }
1426
1427 fn set_filter_config_enabled(&self, enabled: bool) {
1428 self.filter_window_index_lower.set_enabled(enabled);
1429 self.filter_window_index_lower_checkbox.set_enabled(enabled);
1430 self.filter_window_index_upper.set_enabled(enabled);
1431 self.filter_window_index_upper_checkbox.set_enabled(enabled);
1432
1433 self.filter_desktop_index_lower.set_enabled(enabled);
1434 self.filter_desktop_index_lower_checkbox
1435 .set_enabled(enabled);
1436 self.filter_desktop_index_upper.set_enabled(enabled);
1437 self.filter_desktop_index_upper_checkbox
1438 .set_enabled(enabled);
1439
1440 self.filter_title.set_enabled(enabled);
1441
1442 self.filter_process.set_enabled(enabled);
1443
1444 self.filter_action.set_enabled(enabled);
1445
1446 self.filter_target_desktop.set_enabled(enabled);
1447 }
1448 fn set_filter_config_for_sidebar(&self, filter: &WindowFilter) {
1449 fn set_checked(check_box: &nwg::CheckBox, checked: bool) {
1450 check_box.set_check_state(if checked {
1451 nwg::CheckBoxState::Checked
1452 } else {
1453 nwg::CheckBoxState::Unchecked
1454 });
1455 }
1456 fn set_text(text_box: &nwg::TextBox, new_text: &str) {
1457 let new_text = new_text
1458 .chars()
1459 .flat_map(|c| {
1460 [
1461 Some('\r').filter(|_| c == '\n'),
1462 Some(c).filter(|&c| c != '\r'),
1463 ]
1464 })
1465 .flatten()
1466 .collect::<String>();
1467 if text_box.text() != new_text {
1468 text_box.set_text(&new_text);
1469 }
1470 }
1471
1472 set_checked(
1474 &self.filter_window_index_lower_checkbox,
1475 filter.window_index.lower_bound.is_some(),
1476 );
1477 self.filter_window_index_lower
1478 .set_data(nwg::NumberSelectData::Int {
1479 value: filter
1480 .window_index
1481 .lower_bound
1482 .unwrap_or_default()
1483 .saturating_add(1)
1484 .max(1),
1485 step: 1,
1486 max: i64::MAX,
1487 min: 1,
1488 });
1489
1490 set_checked(
1492 &self.filter_window_index_upper_checkbox,
1493 filter.window_index.upper_bound.is_some(),
1494 );
1495 self.filter_window_index_upper
1496 .set_data(nwg::NumberSelectData::Int {
1497 value: filter
1498 .window_index
1499 .upper_bound
1500 .unwrap_or_default()
1501 .saturating_add(1)
1502 .max(1),
1503 step: 1,
1504 max: i64::MAX,
1505 min: 1,
1506 });
1507
1508 set_checked(
1510 &self.filter_desktop_index_lower_checkbox,
1511 filter.desktop_index.lower_bound.is_some(),
1512 );
1513 self.filter_desktop_index_lower
1514 .set_data(nwg::NumberSelectData::Int {
1515 value: filter
1516 .desktop_index
1517 .lower_bound
1518 .unwrap_or_default()
1519 .saturating_add(1)
1520 .max(1),
1521 step: 1,
1522 max: i64::MAX,
1523 min: 1,
1524 });
1525
1526 set_checked(
1528 &self.filter_desktop_index_upper_checkbox,
1529 filter.desktop_index.upper_bound.is_some(),
1530 );
1531 self.filter_desktop_index_upper
1532 .set_data(nwg::NumberSelectData::Int {
1533 value: filter
1534 .desktop_index
1535 .upper_bound
1536 .unwrap_or_default()
1537 .saturating_add(1)
1538 .max(1),
1539 step: 1,
1540 max: i64::MAX,
1541 min: 1,
1542 });
1543
1544 set_text(&self.filter_title, filter.window_title.pattern());
1546
1547 set_text(&self.filter_process, filter.process_name.pattern());
1549
1550 {
1552 let index = self
1553 .filter_action
1554 .collection()
1555 .iter()
1556 .position(|&item| item == filter.action);
1557 self.filter_action.set_selection(index);
1558 }
1559
1560 self.filter_target_desktop
1562 .set_data(nwg::NumberSelectData::Int {
1563 value: filter.target_desktop.saturating_add(1).max(1),
1564 step: 1,
1565 max: i64::MAX,
1566 min: 1,
1567 });
1568 }
1569 fn get_filter_config_for_sidebar(&self) -> Option<WindowFilter> {
1570 Some(WindowFilter {
1571 window_index: {
1572 IntegerRange {
1573 lower_bound: if self.filter_window_index_lower_checkbox.check_state()
1574 != nwg::CheckBoxState::Checked
1575 {
1576 None
1577 } else if let nwg::NumberSelectData::Int { value, .. } =
1578 self.filter_window_index_lower.data()
1579 {
1580 Some(value.saturating_sub(1).max(0))
1581 } else {
1582 return None;
1583 },
1584 upper_bound: if self.filter_window_index_upper_checkbox.check_state()
1585 != nwg::CheckBoxState::Checked
1586 {
1587 None
1588 } else if let nwg::NumberSelectData::Int { value, .. } =
1589 self.filter_window_index_upper.data()
1590 {
1591 Some(value.saturating_sub(1).max(0))
1592 } else {
1593 return None;
1594 },
1595 }
1596 },
1597 desktop_index: {
1598 IntegerRange {
1599 lower_bound: if self.filter_desktop_index_lower_checkbox.check_state()
1600 != nwg::CheckBoxState::Checked
1601 {
1602 None
1603 } else if let nwg::NumberSelectData::Int { value, .. } =
1604 self.filter_desktop_index_lower.data()
1605 {
1606 Some(value.saturating_sub(1).max(0))
1607 } else {
1608 return None;
1609 },
1610 upper_bound: if self.filter_desktop_index_upper_checkbox.check_state()
1611 != nwg::CheckBoxState::Checked
1612 {
1613 None
1614 } else if let nwg::NumberSelectData::Int { value, .. } =
1615 self.filter_desktop_index_upper.data()
1616 {
1617 Some(value.saturating_sub(1).max(0))
1618 } else {
1619 return None;
1620 },
1621 }
1622 },
1623 window_title: TextPattern::new(Arc::from(self.filter_title.text().replace('\r', ""))),
1624 process_name: TextPattern::new(Arc::from(self.filter_process.text().replace('\r', ""))),
1625 action: 'action: {
1626 let Some(selected) = self.filter_action.selection() else {
1627 break 'action FilterAction::default();
1628 };
1629 self.filter_action
1630 .collection()
1631 .get(selected)
1632 .copied()
1633 .unwrap_or_default()
1634 },
1635 target_desktop: if let nwg::NumberSelectData::Int { value, .. } =
1636 self.filter_target_desktop.data()
1637 {
1638 value.saturating_sub(1).max(0)
1639 } else {
1640 return None;
1641 },
1642 })
1643 }
1644
1645 fn get_selected_filter_index(&self) -> Option<usize> {
1646 let nwg::NumberSelectData::Int { value, .. } = self.filter_select_index.data() else {
1647 return None;
1648 };
1649 if value < 1 {
1650 return None;
1651 }
1652 Some((value - 1) as usize)
1653 }
1654
1655 fn on_filter_config_ui_changed(&self) {
1657 let Some(index) = self.get_selected_filter_index() else {
1658 return;
1659 };
1660 let Some(tray) = self.tray.get() else {
1661 return;
1662 };
1663 let Some(new_filter) = self.get_filter_config_for_sidebar() else {
1664 return;
1665 };
1666
1667 tray.settings().update(|prev| UiSettings {
1668 filters: prev
1669 .filters
1670 .iter()
1671 .cloned()
1672 .enumerate()
1673 .map(move |(ix, filter)| {
1674 if ix == index {
1675 new_filter.clone()
1676 } else {
1677 filter
1678 }
1679 })
1680 .collect(),
1681 ..prev.clone()
1682 });
1683 }
1684 fn on_create_filter(&self) {
1685 let Some(tray) = self.tray.get() else {
1686 return;
1687 };
1688 tray.settings().update(|prev| UiSettings {
1689 filters: prev
1690 .filters
1691 .iter()
1692 .cloned()
1693 .chain(Some(WindowFilter::default()))
1694 .collect(),
1695 ..prev.clone()
1696 });
1697 }
1698 fn on_delete_current_filter(&self) {
1699 let Some(index) = self.get_selected_filter_index() else {
1700 return;
1701 };
1702 let Some(tray) = self.tray.get() else {
1703 return;
1704 };
1705 tray.settings().update(|prev| UiSettings {
1706 filters: prev
1707 .filters
1708 .iter()
1709 .enumerate()
1710 .filter(|&(ix, _)| ix != index)
1711 .map(|(_, filter)| filter.clone())
1712 .collect(),
1713 ..prev.clone()
1714 });
1715 }
1716 fn populate_filter_list(&self, filters: &Arc<[WindowFilter]>) {
1717 let prev_filters = self.loaded_filters.borrow().clone();
1718 let prev_filters = prev_filters.as_deref().unwrap_or_default();
1719 let mut indexes_to_skip = Vec::with_capacity(prev_filters.len());
1720
1721 tracing::trace!(
1722 old_filters_count = prev_filters.len(),
1723 new_filters_count = filters.len(),
1724 "ConfigWindow::populate_filter_list"
1725 );
1726
1727 fn get_filter_columns(filter_index: usize, filter: &WindowFilter) -> [String; 6] {
1728 let WindowFilter {
1729 window_index,
1730 desktop_index,
1731 window_title,
1732 process_name,
1733 action: _,
1734 target_desktop: _,
1735 } = filter;
1736
1737 [
1738 window_index.one_based_indexes().to_string(),
1739 desktop_index.one_based_indexes().to_string(),
1740 window_title.display_escaped_newline_glob().to_string(),
1741 process_name.display_escaped_newline_glob().to_string(),
1742 filter_index.saturating_add(1).to_string(),
1743 filter.display_target_desktop().to_string(),
1744 ]
1745 }
1746
1747 for ix in (0..self.data_view.len()).rev() {
1749 let group = list_view_item_get_group_id(&self.data_view, ix);
1750 if group != Self::GROUP_FILTERS {
1751 continue;
1752 }
1753 if let Some(filter_ix) = self.data_view.item(ix, Self::COLUMN_FILTERS_INDEX, 10) {
1754 if let Ok(filter_ix) = filter_ix.text.parse::<usize>() {
1755 let filter_ix = filter_ix - 1;
1757 if let Some(prev) = prev_filters.get(filter_ix) {
1758 if let Some(new) = filters.get(filter_ix) {
1759 if prev != new {
1760 let info = get_filter_columns(filter_ix, new);
1761 for (column_ix, text) in info.into_iter().enumerate() {
1762 self.data_view.update_item(
1763 ix,
1764 nwg::InsertListViewItem {
1765 index: Some(ix as _),
1766 column_index: column_ix as _,
1767 text: Some(text),
1768 image: None,
1769 },
1770 );
1771 }
1772 }
1773 indexes_to_skip.push(filter_ix);
1774 continue;
1775 }
1776 }
1777 }
1778 }
1779 self.data_view.remove_item(ix);
1780 }
1781 tracing::trace!(
1782 updated_filter_indexes = ?indexes_to_skip,
1783 "ConfigWindow::populate_filter_list updated {} items and will create {}",
1784 indexes_to_skip.len(),
1785 filters.len() - indexes_to_skip.len()
1786 );
1787
1788 for (filter_index, filter) in filters.iter().enumerate() {
1790 if indexes_to_skip.contains(&filter_index) {
1791 continue;
1792 }
1793 let info = get_filter_columns(filter_index, filter);
1794 self.data_view.insert_items_row(None, &info);
1795 list_view_item_set_group_id(
1796 &self.data_view,
1797 self.data_view.len().saturating_sub(1),
1798 Some(Self::GROUP_FILTERS),
1799 );
1800 }
1801 self.loaded_filters.replace(Some(filters.clone()));
1802 self.is_data_sorted.set(false);
1803
1804 self.update_window_infos();
1806
1807 self.resort_items();
1808
1809 let selected_filter = if let Some(prev_selected) = self.get_selected_filter_index() {
1811 if prev_selected >= filters.len() {
1812 Some(filters.len().saturating_sub(1))
1814 } else if !prev_filters.is_empty() && filters.len() > prev_filters.len() {
1815 Some(filters.len().saturating_sub(1))
1817 } else {
1818 Some(prev_selected)
1819 }
1820 } else {
1821 None
1822 };
1823 self.set_selected_filter_index(selected_filter);
1824 }
1825 fn sync_filter_from_settings(&self, settings: Option<&Arc<UiSettings>>) {
1826 let settings_owned;
1827 let settings = match settings {
1828 Some(s) => s,
1829 None => {
1830 let Some(tray) = self.tray.get() else {
1831 return;
1832 };
1833 settings_owned = tray.settings().get();
1834 &settings_owned
1835 }
1836 };
1837 self.populate_filter_list(&settings.filters);
1838 }
1839}
1840impl ConfigWindow {
1842 fn on_settings_ui_changed(&self) {
1843 let auto_start = self
1844 .settings_auto_start
1845 .selection()
1846 .and_then(|ix| self.settings_auto_start.collection().get(ix).copied())
1847 .unwrap_or_default();
1848 let tray_icon_type = self
1849 .settings_tray_icon
1850 .selection()
1851 .and_then(|ix| self.settings_tray_icon.collection().get(ix).copied())
1852 .unwrap_or_default();
1853 let quick_switch_menu = self
1854 .settings_quick_menu
1855 .selection()
1856 .and_then(|ix| self.settings_quick_menu.collection().get(ix).copied())
1857 .unwrap_or_default();
1858 let left_click = self
1859 .settings_left_click
1860 .selection()
1861 .and_then(|ix| self.settings_left_click.collection().get(ix).copied())
1862 .unwrap_or_default();
1863 let middle_click = self
1864 .settings_middle_click
1865 .selection()
1866 .and_then(|ix| self.settings_middle_click.collection().get(ix).copied())
1867 .unwrap_or_default();
1868 let mut quick_shortcuts_count = 0;
1869 let mut invalid_quick_shortcut_target = false;
1870 let quick_switch_menu_shortcuts = Arc::new(
1871 self.settings_quick_menu_shortcuts
1872 .text()
1873 .split('\n')
1874 .filter_map(|text| {
1875 let text = text.trim_end_matches('\r');
1878 if text.contains('\r') {
1879 invalid_quick_shortcut_target = true;
1882 }
1883 let text = text.replace('\r', "");
1884 if text.is_empty() {
1885 return None;
1886 }
1887 let (target, key): (String, String) =
1888 text.chars().partition(char::is_ascii_digit);
1889 let target = if target.is_empty() {
1890 invalid_quick_shortcut_target = true;
1892 0
1893 } else {
1894 u32::try_from(
1895 target
1896 .parse::<i64>()
1897 .unwrap_or_else(|_| {
1898 invalid_quick_shortcut_target = true;
1900 0
1901 })
1902 .abs(),
1903 )
1904 .unwrap_or_else(|_| {
1905 invalid_quick_shortcut_target = true;
1907 u32::MAX
1908 })
1909 };
1910 Some((key, target))
1911 })
1912 .inspect(|_| {
1913 quick_shortcuts_count += 1;
1914 })
1915 .collect::<BTreeMap<_, _>>(),
1916 );
1917 let quick_switch_hotkey = Arc::<str>::from(
1918 self.settings_quick_menu_hotkey
1919 .text()
1920 .trim_matches(['\n', '\r']),
1921 );
1922 let open_menu_at_mouse_pos_hotkey = Arc::<str>::from(
1923 self.settings_open_menu_at_mouse_pos_hotkey
1924 .text()
1925 .trim_matches(['\n', '\r']),
1926 );
1927 tracing::debug!(
1928 settings_start_as_admin = ?self.settings_start_as_admin.check_state(),
1929 settings_prevent_flashing_windows = ?self.settings_prevent_flashing_windows.check_state(),
1930 settings_smooth_switch_desktop = ?self.settings_smooth_switch_desktop.check_state(),
1931 ?auto_start,
1932 ?tray_icon_type,
1933 ?quick_switch_menu,
1934 ?quick_switch_menu_shortcuts,
1935 settings_quick_menu_shortcuts_in_submenus =? self.settings_quick_menu_shortcuts_in_submenus.check_state(),
1936 ?quick_switch_hotkey,
1937 ?left_click,
1938 ?middle_click,
1939 ?open_menu_at_mouse_pos_hotkey,
1940 "ConfigWindow::on_settings_ui_changed"
1941 );
1942 if invalid_quick_shortcut_target
1943 || quick_shortcuts_count != quick_switch_menu_shortcuts.len()
1944 {
1945 tracing::debug!(
1947 "Invalid numbers or duplicated items in quick switch shortcuts field, \
1948 restoring to current settings value"
1949 );
1950 self.sync_quick_shortcuts_from(&quick_switch_menu_shortcuts);
1951 }
1952 let Some(tray) = self.tray.get() else {
1953 return;
1954 };
1955 tray.settings().update(|prev| UiSettings {
1956 request_admin_at_startup: self.settings_start_as_admin.check_state()
1957 == nwg::CheckBoxState::Checked,
1958 auto_start,
1959 stop_flashing_windows_after_applying_filter: self
1960 .settings_prevent_flashing_windows
1961 .check_state()
1962 == nwg::CheckBoxState::Checked,
1963 smooth_switch_desktops: self.settings_smooth_switch_desktop.check_state()
1964 == nwg::CheckBoxState::Checked,
1965 tray_icon_type,
1966 quick_switch_menu,
1967 quick_switch_menu_shortcuts,
1968 quick_switch_menu_shortcuts_only_in_root: self
1969 .settings_quick_menu_shortcuts_in_submenus
1970 .check_state()
1971 != nwg::CheckBoxState::Checked,
1972 quick_switch_hotkey,
1973 left_click,
1974 middle_click,
1975 open_menu_at_mouse_pos_hotkey,
1976 ..prev.clone()
1977 });
1978 }
1979 fn sync_program_options_from_settings(&self, settings: Option<&Arc<UiSettings>>) {
1980 let settings_owned;
1981 let settings = match settings {
1982 Some(s) => s,
1983 None => {
1984 let Some(tray) = self.tray.get() else {
1985 return;
1986 };
1987 settings_owned = tray.settings().get();
1988 &settings_owned
1989 }
1990 };
1991 fn set_checked(check_box: &nwg::CheckBox, checked: bool) {
1992 check_box.set_check_state(if checked {
1993 nwg::CheckBoxState::Checked
1994 } else {
1995 nwg::CheckBoxState::Unchecked
1996 });
1997 }
1998 set_checked(
1999 &self.settings_start_as_admin,
2000 settings.request_admin_at_startup,
2001 );
2002 {
2003 let index = self
2004 .settings_auto_start
2005 .collection()
2006 .iter()
2007 .position(|&item| item == settings.auto_start);
2008 self.settings_auto_start.set_selection(index);
2009 }
2010 set_checked(
2011 &self.settings_prevent_flashing_windows,
2012 settings.stop_flashing_windows_after_applying_filter,
2013 );
2014 set_checked(
2015 &self.settings_smooth_switch_desktop,
2016 settings.smooth_switch_desktops,
2017 );
2018 {
2019 let index = self
2020 .settings_tray_icon
2021 .collection()
2022 .iter()
2023 .position(|&item| item == settings.tray_icon_type);
2024 self.settings_tray_icon.set_selection(index);
2025 }
2026 {
2027 let index = self
2028 .settings_quick_menu
2029 .collection()
2030 .iter()
2031 .position(|&item| item == settings.quick_switch_menu);
2032 self.settings_quick_menu.set_selection(index);
2033 }
2034 self.sync_quick_shortcuts_from(&settings.quick_switch_menu_shortcuts);
2035 set_checked(
2036 &self.settings_quick_menu_shortcuts_in_submenus,
2037 !settings.quick_switch_menu_shortcuts_only_in_root,
2038 );
2039 {
2040 let new_text = &*settings.quick_switch_hotkey;
2041 if new_text != self.settings_quick_menu_hotkey.text() {
2042 self.settings_quick_menu_hotkey.set_text(new_text);
2043 }
2044 self.settings_quick_menu_hotkey_error.set_text(&{
2045 if settings.quick_switch_hotkey.is_empty() {
2046 "Hotkey disabled".to_owned()
2047 } else {
2048 #[cfg(feature = "global_hotkey")]
2049 {
2050 match global_hotkey::hotkey::HotKey::from_str(&settings.quick_switch_hotkey)
2051 {
2052 Ok(_) => "Valid hotkey".to_owned(),
2053 Err(e) => format!("Invalid hotkey: {e}"),
2054 }
2055 }
2056 #[cfg(not(feature = "global_hotkey"))]
2057 {
2058 "Compiled without hotkey support".to_owned()
2059 }
2060 }
2061 });
2062 }
2063 {
2064 let index = self
2065 .settings_left_click
2066 .collection()
2067 .iter()
2068 .position(|&item| item == settings.left_click);
2069 self.settings_left_click.set_selection(index);
2070 }
2071 {
2072 let index = self
2073 .settings_middle_click
2074 .collection()
2075 .iter()
2076 .position(|&item| item == settings.middle_click);
2077 self.settings_middle_click.set_selection(index);
2078 }
2079 {
2080 let new_text = &*settings.open_menu_at_mouse_pos_hotkey;
2081 if new_text != self.settings_open_menu_at_mouse_pos_hotkey.text() {
2082 self.settings_open_menu_at_mouse_pos_hotkey
2083 .set_text(new_text);
2084 }
2085 self.settings_open_menu_at_mouse_pos_hotkey_error
2086 .set_text(&{
2087 if settings.open_menu_at_mouse_pos_hotkey.is_empty() {
2088 "Hotkey disabled".to_owned()
2089 } else {
2090 #[cfg(feature = "global_hotkey")]
2091 {
2092 match global_hotkey::hotkey::HotKey::from_str(
2093 &settings.open_menu_at_mouse_pos_hotkey,
2094 ) {
2095 Ok(_) => "Valid hotkey".to_owned(),
2096 Err(e) => format!("Invalid hotkey: {e}"),
2097 }
2098 }
2099 #[cfg(not(feature = "global_hotkey"))]
2100 {
2101 "Compiled without hotkey support".to_owned()
2102 }
2103 }
2104 });
2105 }
2106 }
2107 fn sync_quick_shortcuts_from(&self, shortcuts: &BTreeMap<String, u32>) {
2108 let selection = self.settings_quick_menu_shortcuts.selection();
2109 let text = shortcuts.iter().fold(
2110 String::with_capacity(shortcuts.len() * 4),
2111 |mut f, (mut key, target)| {
2112 let newlines = ['\r', '\n'];
2114 let new_key;
2115 if key.contains(newlines) {
2116 new_key = key.replace(newlines, "");
2117 key = &new_key;
2118 }
2119
2120 use std::fmt::Write;
2121 write!(f, "{}{}\r\n", key, target)
2122 .expect("should succeed at writing to in-memory string");
2123 f
2124 },
2125 );
2126 self.settings_quick_menu_shortcuts.set_text(&text);
2127 let mut selection =
2128 selection.start.min(text.len() as u32)..selection.end.min(text.len() as u32);
2129
2130 let Some(selected_and_prev) = text
2132 .as_bytes()
2133 .get((selection.start as usize).saturating_sub(1)..(selection.end as usize))
2134 else {
2135 tracing::warn!(
2136 ?selection,
2137 text,
2138 "Selection was over invalid characters so can't update it"
2139 );
2140 return;
2141 };
2142 tracing::debug!(
2143 selected_and_prev =? String::from_utf8_lossy(selected_and_prev),
2144 range =? selection,
2145 "Updating Quick switch shortcut text box selection"
2146 );
2147 if selected_and_prev.starts_with(b"\r") {
2148 selection.start = selection.start.saturating_sub(1);
2149 if selected_and_prev.len() == 1 {
2150 selection.end = selection.end.saturating_sub(1);
2151 }
2152 }
2153 if selected_and_prev.len() > 1 && selected_and_prev.ends_with(b"\r") {
2154 selection.end = selection.end.saturating_add(1).min(text.len() as u32);
2155 }
2156
2157 self.settings_quick_menu_shortcuts.set_selection(selection);
2158 }
2159}
2160impl DynamicUiHooks<SystemTray> for ConfigWindow {
2161 fn before_partial_build(
2162 &mut self,
2163 dynamic_ui: &Rc<SystemTray>,
2164 should_build: &mut bool,
2165 ) -> Option<(nwg::ControlHandle, std::any::TypeId)> {
2166 self.tray.set(dynamic_ui);
2167 if !self.open_soon.replace(false) {
2168 *should_build = false;
2169 }
2170 None
2171 }
2172 fn after_partial_build(&mut self, _dynamic_ui: &Rc<SystemTray>) {
2173 if let Err(e) = self.build_layout() {
2174 tracing::error!(
2175 error = e.to_string(),
2176 "Failed to build layout for ConfigWindow"
2177 );
2178 }
2179 if let Err(e) = self.build_tooltip() {
2180 tracing::error!(
2181 error = e.to_string(),
2182 "Failed to build tooltips for ConfigWindow"
2183 );
2184 }
2185
2186 self.sync_program_options_from_settings(None);
2187 self.set_as_foreground_window();
2188 }
2189 fn after_handles<'a>(
2190 &'a self,
2191 _dynamic_ui: &Rc<SystemTray>,
2192 handles: &mut Vec<&'a nwg::ControlHandle>,
2193 ) {
2194 *handles = vec![&self.window.handle];
2195 }
2196
2197 fn need_rebuild(&self, _dynamic_ui: &Rc<SystemTray>) -> bool {
2198 self.open_soon.get() && self.is_closed()
2200 }
2201 fn is_ordered_in_parent(&self) -> bool {
2202 false
2203 }
2204 fn before_rebuild(&mut self, _dynamic_ui: &Rc<SystemTray>) {
2205 let export_dialog = std::mem::take(&mut self.export_dialog);
2206 let import_dialog = std::mem::take(&mut self.import_dialog);
2207 *self = Default::default();
2208 self.export_dialog = export_dialog;
2209 self.import_dialog = import_dialog;
2210 self.open_soon = Cell::new(true);
2213 }
2214}
2215impl TrayPlugin for ConfigWindow {
2216 fn on_settings_changed(
2217 &self,
2218 _tray_ui: &Rc<SystemTray>,
2219 _prev: &Arc<UiSettings>,
2220 new: &Arc<UiSettings>,
2221 ) {
2222 self.sync_program_options_from_settings(Some(new));
2223 let has_changed_filters =
2224 self.loaded_filters.borrow().as_deref().unwrap_or_default() != &*new.filters;
2225 if has_changed_filters {
2226 self.sync_filter_from_settings(Some(new));
2227 }
2228 }
2229}