Skip to main content

cadmus_core/view/settings_editor/kinds/
intermission.rs

1//! Setting kinds for the Intermissions category.
2
3use super::{SettingData, SettingIdentity, SettingKind, WidgetKind};
4use crate::fl;
5use crate::i18n::I18nDisplay;
6use crate::settings::{IntermKind, IntermissionDisplay, Settings};
7use crate::view::{Bus, EntryId, EntryKind, Event};
8
9/// Fetches the intermission display setting data for the given intermission kind
10fn fetch_intermission(kind: IntermKind, settings: &Settings) -> SettingData {
11    let display = &settings.intermissions[kind];
12    let value = intermission_display_name(display);
13    let selected = selected_builtin_intermission(kind, display);
14    let is_selected = |candidate| selected.as_ref() == Some(&candidate);
15
16    let mut entries = vec![
17        EntryKind::RadioButton(
18            IntermissionDisplay::Logo.to_i18n_string(),
19            EntryId::SetIntermission(kind, IntermissionDisplay::Logo),
20            is_selected(IntermissionDisplay::Logo),
21        ),
22        EntryKind::RadioButton(
23            IntermissionDisplay::Cover.to_i18n_string(),
24            EntryId::SetIntermission(kind, IntermissionDisplay::Cover),
25            is_selected(IntermissionDisplay::Cover),
26        ),
27        EntryKind::RadioButton(
28            IntermissionDisplay::Blank.to_i18n_string(),
29            EntryId::SetIntermission(kind, IntermissionDisplay::Blank),
30            is_selected(IntermissionDisplay::Blank),
31        ),
32        EntryKind::RadioButton(
33            IntermissionDisplay::BlankInverted.to_i18n_string(),
34            EntryId::SetIntermission(kind, IntermissionDisplay::BlankInverted),
35            is_selected(IntermissionDisplay::BlankInverted),
36        ),
37    ];
38
39    if kind.supports_calendar() {
40        entries.push(EntryKind::RadioButton(
41            IntermissionDisplay::Calendar.to_i18n_string(),
42            EntryId::SetIntermission(kind, IntermissionDisplay::Calendar),
43            is_selected(IntermissionDisplay::Calendar),
44        ));
45    }
46
47    entries.push(EntryKind::Command(
48        fl!("settings-intermission-custom-image"),
49        EntryId::EditIntermissionImage(kind),
50    ));
51
52    SettingData {
53        value,
54        widget: WidgetKind::SubMenu(entries),
55    }
56}
57
58/// Extracts the display name from an [`IntermissionDisplay`] value.
59///
60/// Uses [`IntermissionDisplay`]'s [`I18nDisplay`] implementation for
61/// `Logo` and `Cover`. For `Image`, the filename is used instead of the full path since
62/// the built-in `Display` impl only yields `"Custom"` for that variant.
63fn intermission_display_name(display: &IntermissionDisplay) -> String {
64    let i18n_display = fl!("settings-intermission-custom");
65    match display {
66        IntermissionDisplay::Image(path) => path
67            .file_name()
68            .and_then(|n| n.to_str())
69            .unwrap_or(i18n_display.as_str())
70            .to_string(),
71        _ => display.to_i18n_string(),
72    }
73}
74
75fn selected_builtin_intermission(
76    kind: IntermKind,
77    display: &IntermissionDisplay,
78) -> Option<IntermissionDisplay> {
79    match display {
80        IntermissionDisplay::Image(_) => None,
81        IntermissionDisplay::Calendar if !kind.supports_calendar() => {
82            Some(IntermissionDisplay::Logo)
83        }
84        _ => Some(display.clone()),
85    }
86}
87
88/// Suspend screen display setting
89pub struct IntermissionSuspend;
90
91impl SettingKind for IntermissionSuspend {
92    fn identity(&self) -> SettingIdentity {
93        SettingIdentity::IntermissionSuspend
94    }
95
96    fn label(&self, _settings: &Settings) -> String {
97        fl!("settings-intermission-suspend-screen")
98    }
99
100    fn fetch(&self, settings: &Settings) -> SettingData {
101        fetch_intermission(IntermKind::Suspend, settings)
102    }
103
104    fn handle(
105        &self,
106        evt: &Event,
107        settings: &mut Settings,
108        _bus: &mut Bus,
109    ) -> (Option<String>, bool) {
110        if let Event::Select(EntryId::SetIntermission(IntermKind::Suspend, ref display)) = evt {
111            if !settings
112                .intermissions
113                .set_display(IntermKind::Suspend, display.clone())
114            {
115                return (None, true);
116            }
117
118            return (Some(intermission_display_name(display)), true);
119        }
120
121        if let Event::FileChooserClosed(Some(ref path)) = evt {
122            let display = IntermissionDisplay::Image(path.clone());
123            settings.intermissions[IntermKind::Suspend] = display.clone();
124            return (Some(intermission_display_name(&display)), true);
125        }
126
127        (None, false)
128    }
129
130    fn file_chooser_entry_id(&self) -> Option<EntryId> {
131        Some(EntryId::EditIntermissionImage(IntermKind::Suspend))
132    }
133}
134
135/// Power off screen display setting
136pub struct IntermissionPowerOff;
137
138impl SettingKind for IntermissionPowerOff {
139    fn identity(&self) -> SettingIdentity {
140        SettingIdentity::IntermissionPowerOff
141    }
142
143    fn label(&self, _settings: &Settings) -> String {
144        fl!("settings-intermission-power-off-screen")
145    }
146
147    fn fetch(&self, settings: &Settings) -> SettingData {
148        fetch_intermission(IntermKind::PowerOff, settings)
149    }
150
151    fn handle(
152        &self,
153        evt: &Event,
154        settings: &mut Settings,
155        _bus: &mut Bus,
156    ) -> (Option<String>, bool) {
157        if let Event::Select(EntryId::SetIntermission(IntermKind::PowerOff, ref display)) = evt {
158            if !settings
159                .intermissions
160                .set_display(IntermKind::PowerOff, display.clone())
161            {
162                return (None, true);
163            }
164
165            return (Some(intermission_display_name(display)), true);
166        }
167
168        if let Event::FileChooserClosed(Some(ref path)) = evt {
169            let display = IntermissionDisplay::Image(path.clone());
170            settings.intermissions[IntermKind::PowerOff] = display.clone();
171            return (Some(intermission_display_name(&display)), true);
172        }
173
174        (None, false)
175    }
176
177    fn file_chooser_entry_id(&self) -> Option<EntryId> {
178        Some(EntryId::EditIntermissionImage(IntermKind::PowerOff))
179    }
180}
181
182/// Share screen display setting
183pub struct IntermissionShare;
184
185impl SettingKind for IntermissionShare {
186    fn identity(&self) -> SettingIdentity {
187        SettingIdentity::IntermissionShare
188    }
189
190    fn label(&self, _settings: &Settings) -> String {
191        fl!("settings-intermission-share-screen")
192    }
193
194    fn fetch(&self, settings: &Settings) -> SettingData {
195        fetch_intermission(IntermKind::Share, settings)
196    }
197
198    fn handle(
199        &self,
200        evt: &Event,
201        settings: &mut Settings,
202        _bus: &mut Bus,
203    ) -> (Option<String>, bool) {
204        if let Event::Select(EntryId::SetIntermission(IntermKind::Share, ref display)) = evt {
205            if !settings
206                .intermissions
207                .set_display(IntermKind::Share, display.clone())
208            {
209                return (None, true);
210            }
211
212            return (Some(intermission_display_name(display)), true);
213        }
214
215        if let Event::FileChooserClosed(Some(ref path)) = evt {
216            let display = IntermissionDisplay::Image(path.clone());
217            settings.intermissions[IntermKind::Share] = display.clone();
218            return (Some(intermission_display_name(&display)), true);
219        }
220
221        (None, false)
222    }
223
224    fn file_chooser_entry_id(&self) -> Option<EntryId> {
225        Some(EntryId::EditIntermissionImage(IntermKind::Share))
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::settings::{IntermissionDisplay, Settings};
233    use crate::view::{Bus, EntryId, Event};
234    use std::collections::VecDeque;
235    use std::path::PathBuf;
236
237    mod intermission_suspend {
238        use super::*;
239        use crate::view::EntryKind;
240
241        #[test]
242        fn handle_set_intermission_updates_settings() {
243            let setting = IntermissionSuspend;
244            let mut settings = Settings::default();
245            let mut bus: Bus = VecDeque::new();
246            let event = Event::Select(EntryId::SetIntermission(
247                IntermKind::Suspend,
248                IntermissionDisplay::Cover,
249            ));
250
251            let result = setting.handle(&event, &mut settings, &mut bus);
252
253            assert!(result.0.is_some());
254            assert_eq!(
255                settings.intermissions[IntermKind::Suspend],
256                IntermissionDisplay::Cover
257            );
258        }
259
260        #[test]
261        fn handle_file_chooser_closed_updates_settings() {
262            let setting = IntermissionSuspend;
263            let mut settings = Settings::default();
264            let mut bus: Bus = VecDeque::new();
265            let path = PathBuf::from("/selected/image.jpg");
266            let event = Event::FileChooserClosed(Some(path));
267
268            let result = setting.handle(&event, &mut settings, &mut bus);
269
270            assert!(result.0.is_some());
271            assert_eq!(
272                settings.intermissions[IntermKind::Suspend],
273                IntermissionDisplay::Image(PathBuf::from("/selected/image.jpg"))
274            );
275        }
276
277        #[test]
278        fn handle_returns_none_for_wrong_kind() {
279            let setting = IntermissionSuspend;
280            let mut settings = Settings::default();
281            let mut bus: Bus = VecDeque::new();
282            let event = Event::Select(EntryId::SetIntermission(
283                IntermKind::PowerOff,
284                IntermissionDisplay::Cover,
285            ));
286
287            let result = setting.handle(&event, &mut settings, &mut bus);
288
289            assert!(result.0.is_none());
290        }
291
292        #[test]
293        fn handle_returns_none_for_cancelled_file_chooser() {
294            let setting = IntermissionSuspend;
295            let mut settings = Settings::default();
296            let mut bus: Bus = VecDeque::new();
297
298            let result = setting.handle(&Event::FileChooserClosed(None), &mut settings, &mut bus);
299
300            assert!(result.0.is_none());
301        }
302
303        #[test]
304        fn fetch_includes_calendar_option() {
305            let setting = IntermissionSuspend;
306            let settings = Settings::default();
307            let data = setting.fetch(&settings);
308
309            let WidgetKind::SubMenu(entries) = data.widget else {
310                panic!("expected submenu widget");
311            };
312
313            assert!(entries.iter().any(|entry| {
314                matches!(
315                    entry,
316                    EntryKind::RadioButton(
317                        _,
318                        EntryId::SetIntermission(
319                            IntermKind::Suspend,
320                            IntermissionDisplay::Calendar
321                        ),
322                        _
323                    )
324                )
325            }));
326        }
327
328        #[test]
329        fn fetch_includes_blank_options() {
330            let setting = IntermissionSuspend;
331            let settings = Settings::default();
332            let data = setting.fetch(&settings);
333
334            let WidgetKind::SubMenu(entries) = data.widget else {
335                panic!("expected submenu widget");
336            };
337
338            assert!(entries.iter().any(|entry| {
339                matches!(
340                    entry,
341                    EntryKind::RadioButton(
342                        _,
343                        EntryId::SetIntermission(IntermKind::Suspend, IntermissionDisplay::Blank),
344                        _
345                    )
346                )
347            }));
348
349            assert!(entries.iter().any(|entry| {
350                matches!(
351                    entry,
352                    EntryKind::RadioButton(
353                        _,
354                        EntryId::SetIntermission(
355                            IntermKind::Suspend,
356                            IntermissionDisplay::BlankInverted
357                        ),
358                        _
359                    )
360                )
361            }));
362        }
363    }
364
365    mod intermission_power_off {
366        use super::*;
367        use crate::view::EntryKind;
368
369        #[test]
370        fn handle_set_intermission_updates_settings() {
371            let setting = IntermissionPowerOff;
372            let mut settings = Settings::default();
373            let mut bus: Bus = VecDeque::new();
374            let event = Event::Select(EntryId::SetIntermission(
375                IntermKind::PowerOff,
376                IntermissionDisplay::Cover,
377            ));
378
379            let result = setting.handle(&event, &mut settings, &mut bus);
380
381            assert!(result.0.is_some());
382            assert_eq!(
383                settings.intermissions[IntermKind::PowerOff],
384                IntermissionDisplay::Cover
385            );
386        }
387
388        #[test]
389        fn handle_file_chooser_closed_updates_settings() {
390            let setting = IntermissionPowerOff;
391            let mut settings = Settings::default();
392            let mut bus: Bus = VecDeque::new();
393            let path = PathBuf::from("/selected/poweroff.png");
394            let event = Event::FileChooserClosed(Some(path));
395
396            let result = setting.handle(&event, &mut settings, &mut bus);
397
398            assert!(result.0.is_some());
399            assert_eq!(
400                settings.intermissions[IntermKind::PowerOff],
401                IntermissionDisplay::Image(PathBuf::from("/selected/poweroff.png"))
402            );
403        }
404
405        #[test]
406        fn handle_returns_none_for_wrong_kind() {
407            let setting = IntermissionPowerOff;
408            let mut settings = Settings::default();
409            let mut bus: Bus = VecDeque::new();
410            let event = Event::Select(EntryId::SetIntermission(
411                IntermKind::Suspend,
412                IntermissionDisplay::Logo,
413            ));
414
415            let result = setting.handle(&event, &mut settings, &mut bus);
416
417            assert!(result.0.is_none());
418        }
419
420        #[test]
421        fn handle_returns_none_for_cancelled_file_chooser() {
422            let setting = IntermissionPowerOff;
423            let mut settings = Settings::default();
424            let mut bus: Bus = VecDeque::new();
425
426            let result = setting.handle(&Event::FileChooserClosed(None), &mut settings, &mut bus);
427
428            assert!(result.0.is_none());
429        }
430
431        #[test]
432        fn handle_rejects_calendar_selection() {
433            let setting = IntermissionPowerOff;
434            let mut settings = Settings::default();
435            let mut bus: Bus = VecDeque::new();
436            let event = Event::Select(EntryId::SetIntermission(
437                IntermKind::PowerOff,
438                IntermissionDisplay::Calendar,
439            ));
440
441            let result = setting.handle(&event, &mut settings, &mut bus);
442
443            assert_eq!(result, (None, true));
444            assert_eq!(
445                settings.intermissions[IntermKind::PowerOff],
446                IntermissionDisplay::Logo
447            );
448        }
449
450        #[test]
451        fn handle_accepts_blank_selection() {
452            let setting = IntermissionPowerOff;
453            let mut settings = Settings::default();
454            let mut bus: Bus = VecDeque::new();
455            let event = Event::Select(EntryId::SetIntermission(
456                IntermKind::PowerOff,
457                IntermissionDisplay::Blank,
458            ));
459
460            let result = setting.handle(&event, &mut settings, &mut bus);
461
462            assert_eq!(result, (Some(fl!("settings-intermission-blank")), true));
463            assert_eq!(
464                settings.intermissions[IntermKind::PowerOff],
465                IntermissionDisplay::Blank
466            );
467        }
468
469        #[test]
470        fn fetch_excludes_calendar_option() {
471            let setting = IntermissionPowerOff;
472            let settings = Settings::default();
473            let data = setting.fetch(&settings);
474
475            let WidgetKind::SubMenu(entries) = data.widget else {
476                panic!("expected submenu widget");
477            };
478
479            assert!(!entries.iter().any(|entry| {
480                matches!(
481                    entry,
482                    EntryKind::RadioButton(
483                        _,
484                        EntryId::SetIntermission(
485                            IntermKind::PowerOff,
486                            IntermissionDisplay::Calendar
487                        ),
488                        _
489                    )
490                )
491            }));
492        }
493    }
494
495    mod intermission_share {
496        use super::*;
497        use crate::view::EntryKind;
498
499        #[test]
500        fn handle_set_intermission_updates_settings() {
501            let setting = IntermissionShare;
502            let mut settings = Settings::default();
503            let mut bus: Bus = VecDeque::new();
504            let event = Event::Select(EntryId::SetIntermission(
505                IntermKind::Share,
506                IntermissionDisplay::Cover,
507            ));
508
509            let result = setting.handle(&event, &mut settings, &mut bus);
510
511            assert!(result.0.is_some());
512            assert_eq!(
513                settings.intermissions[IntermKind::Share],
514                IntermissionDisplay::Cover
515            );
516        }
517
518        #[test]
519        fn handle_file_chooser_closed_updates_settings() {
520            let setting = IntermissionShare;
521            let mut settings = Settings::default();
522            let mut bus: Bus = VecDeque::new();
523            let path = PathBuf::from("/selected/share.jpg");
524            let event = Event::FileChooserClosed(Some(path));
525
526            let result = setting.handle(&event, &mut settings, &mut bus);
527
528            assert!(result.0.is_some());
529            assert_eq!(
530                settings.intermissions[IntermKind::Share],
531                IntermissionDisplay::Image(PathBuf::from("/selected/share.jpg"))
532            );
533        }
534
535        #[test]
536        fn handle_returns_none_for_wrong_kind() {
537            let setting = IntermissionShare;
538            let mut settings = Settings::default();
539            let mut bus: Bus = VecDeque::new();
540            let event = Event::Select(EntryId::SetIntermission(
541                IntermKind::PowerOff,
542                IntermissionDisplay::Cover,
543            ));
544
545            let result = setting.handle(&event, &mut settings, &mut bus);
546
547            assert!(result.0.is_none());
548        }
549
550        #[test]
551        fn handle_returns_none_for_cancelled_file_chooser() {
552            let setting = IntermissionShare;
553            let mut settings = Settings::default();
554            let mut bus: Bus = VecDeque::new();
555
556            let result = setting.handle(&Event::FileChooserClosed(None), &mut settings, &mut bus);
557
558            assert!(result.0.is_none());
559        }
560
561        #[test]
562        fn handle_rejects_calendar_selection() {
563            let setting = IntermissionShare;
564            let mut settings = Settings::default();
565            let mut bus: Bus = VecDeque::new();
566            let event = Event::Select(EntryId::SetIntermission(
567                IntermKind::Share,
568                IntermissionDisplay::Calendar,
569            ));
570
571            let result = setting.handle(&event, &mut settings, &mut bus);
572
573            assert_eq!(result, (None, true));
574            assert_eq!(
575                settings.intermissions[IntermKind::Share],
576                IntermissionDisplay::Logo
577            );
578        }
579
580        #[test]
581        fn handle_accepts_blank_inverted_selection() {
582            let setting = IntermissionShare;
583            let mut settings = Settings::default();
584            let mut bus: Bus = VecDeque::new();
585            let event = Event::Select(EntryId::SetIntermission(
586                IntermKind::Share,
587                IntermissionDisplay::BlankInverted,
588            ));
589
590            let result = setting.handle(&event, &mut settings, &mut bus);
591
592            assert_eq!(
593                result,
594                (Some(fl!("settings-intermission-blank-inverted")), true)
595            );
596            assert_eq!(
597                settings.intermissions[IntermKind::Share],
598                IntermissionDisplay::BlankInverted
599            );
600        }
601
602        #[test]
603        fn fetch_excludes_calendar_option() {
604            let setting = IntermissionShare;
605            let settings = Settings::default();
606            let data = setting.fetch(&settings);
607
608            let WidgetKind::SubMenu(entries) = data.widget else {
609                panic!("expected submenu widget");
610            };
611
612            assert!(!entries.iter().any(|entry| {
613                matches!(
614                    entry,
615                    EntryKind::RadioButton(
616                        _,
617                        EntryId::SetIntermission(IntermKind::Share, IntermissionDisplay::Calendar),
618                        _
619                    )
620                )
621            }));
622        }
623    }
624}