Skip to main content

zng_wgt_setup/page/
optional_tasks.rs

1use zng_ext_l10n::l10n;
2use zng_var::MergeVarBuilder;
3use zng_wgt::{margin, prelude::*};
4use zng_wgt_container::Container;
5use zng_wgt_scroll::{Scroll, ScrollMode};
6use zng_wgt_stack::{Stack, StackDirection};
7use zng_wgt_text::Text;
8use zng_wgt_toggle::Toggle;
9use zng_wgt_wizard::Page;
10
11use crate::APP_NAME_VAR;
12
13/// Page for reviewing and enabling optional setup tasks.
14///
15/// The *tasks* represented here do not need to match a `SetupTask` each, usually
16/// the entire page configures a single `SetupTask`.
17#[non_exhaustive]
18pub struct OptionalTasksPage {
19    /// Page title.
20    ///
21    /// This is the [`Page::title`] value.
22    pub title: Var<Txt>,
23
24    /// Page info.
25    ///
26    /// This is the [`Page::info`] value.
27    pub info: Var<Txt>,
28
29    /// Message shown at the top of the page content.
30    pub message: Var<Txt>,
31
32    /// List of optional tasks.
33    ///
34    /// Tasks are presented as a list of check boxes, in the same order as this vec.
35    /// In case of grouping the first task of that group defines the group position.
36    pub tasks: Vec<OptionalTask>,
37
38    /// Info about `tasks` groups.
39    ///
40    /// Groups are presented as a check box header that can enable or disable all child
41    /// items at the same time.
42    pub groups: Vec<OptionalTaskGroup>,
43}
44/// Common task pages.
45///
46/// Note that to support pluralization based on `tasks` count the `title`, `info` and `message` vars are set
47/// to a marker on `new_*`, and are resolved only on `build`.
48impl OptionalTasksPage {
49    /// New generic "Select Adicional Tasks" page.
50    pub fn new_generic() -> Self {
51        Self {
52            title: const_var("generic".into()),
53            info: const_var("generic".into()),
54            message: const_var("generic".into()),
55            tasks: vec![],
56            groups: vec![],
57        }
58    }
59
60    /// New "Associate File Types" page.
61    pub fn new_associate_files() -> Self {
62        Self {
63            title: const_var("associate_files".into()),
64            info: const_var("associate_files".into()),
65            message: const_var("associate_files".into()),
66            tasks: vec![],
67            groups: vec![],
68        }
69    }
70
71    /// New "Additional Shortcuts" page.
72    ///
73    /// Note that at least one shortcut should always be created. On Windows for example, an
74    /// app is not registered to show notifications if it does not have a start menu shortcut.
75    /// This page is for extra shortcuts, like [`OptionalTask::new_create_desktop_shortcut`].
76    pub fn new_additional_shortcuts() -> Self {
77        Self {
78            title: const_var("additional_shortcuts".into()),
79            info: const_var("additional_shortcuts".into()),
80            message: const_var("".into()),
81            tasks: vec![],
82            groups: vec![],
83        }
84    }
85}
86impl OptionalTasksPage {
87    fn resolve_builtin(&mut self) {
88        fn marker(v: &Var<Txt>) -> Option<&'static str> {
89            if v.capabilities().is_const() {
90                v.get().as_static_str()
91            } else {
92                None
93            }
94        }
95
96        let tasks = self.tasks.len();
97        let app = APP_NAME_VAR;
98
99        match marker(&self.title) {
100            Some("generic") => self.title = l10n!("optional_tasks/generic.title", "Select Additional Tasks"),
101            Some("associate_files") => {
102                self.title = l10n!(
103                    "optional_tasks/associate_files.title",
104                    "{ $tasks -> 
105                    [one] Associate File Type
106                    *[other] Associate File Types 
107                    }"
108                )
109            }
110            Some("additional_shortcuts") => {
111                self.title = l10n!(
112                    "optional_tasks/additional_shortcuts.title",
113                    "{ $tasks -> 
114                    [one] Additional Shortcut
115                    *[other] Additional Shortcuts
116                    }"
117                )
118            }
119            _ => {}
120        }
121        match marker(&self.info) {
122            Some("generic") => self.info = l10n!("optional_tasks/generic.info", "Which additional tasks should be performed?"),
123            Some("associate_files") => {
124                self.info = l10n!(
125                    "optional_tasks/associate_files.info",
126                    "{ $tasks ->
127                    [one] Should this file type open with {$app} by default?
128                    *[other] Which file types should open with {$app} by default?
129                    }",
130                )
131            }
132            Some("additional_shortcuts") => {
133                self.info = l10n!(
134                    "optional_tasks/additional_shortcuts.info",
135                    "{ $tasks ->
136                    [one] Create additional shortcut?
137                    *[other] Which additional shortcuts should be created? 
138                    }",
139                )
140            }
141            _ => {}
142        }
143        match marker(&self.message) {
144            Some("generic") => {
145                self.message = l10n!(
146                    "optional_tasks/generic.message",
147                    "Select additional tasks you would like to setup to perform."
148                )
149            }
150            Some("associate_files") => {
151                // l10n-# Message only shows for $tasks > 1
152                self.message = l10n!(
153                    "optional_tasks/associate_files.message",
154                    "{ $tasks -> 
155                        *[other] Select file types you would like associate with {$app}.
156                        }",
157                )
158            }
159            _ => {}
160        }
161    }
162
163    /// Build the page.
164    ///
165    /// # Panics
166    ///
167    /// Panics if no tasks are defined or if a task requests an undefined group.
168    pub fn build(mut self) -> Page {
169        assert!(!self.tasks.is_empty());
170
171        self.resolve_builtin();
172
173        // groups and items are displayed on the same list, with margin adjusted to define group children
174        let mut items = Vec::with_capacity(self.tasks.len() + self.groups.len());
175        enum Item {
176            Task(Var<Txt>, Var<bool>),
177            GroupHeader(Var<Txt>, Var<Option<bool>>),
178            GroupTask(Var<Txt>, Var<bool>),
179            GroupReserved,
180        }
181        let mut taken = vec![false; self.tasks.len()];
182        for i in 0..self.tasks.len() {
183            if taken[i] {
184                continue;
185            }
186            taken[i] = true;
187
188            let task = &self.tasks[i];
189            if task.group_id.is_empty() {
190                items.push(Item::Task(task.name.clone(), task.enabled.clone()));
191            } else {
192                let group = self
193                    .groups
194                    .iter()
195                    .find(|g| g.id == task.group_id)
196                    .unwrap_or_else(|| panic!("group {:?} not defined", task.group_id));
197                let group_i = items.len();
198                let mut group_enabled = MergeVarBuilder::new();
199
200                items.push(Item::GroupReserved);
201
202                let mut modifiable = task.enabled.capabilities().can_modify();
203
204                group_enabled.push(task.enabled.clone());
205                items.push(Item::GroupTask(task.name.clone(), task.enabled.clone()));
206
207                #[allow(clippy::needless_range_loop)] // false positive
208                for i in (i + 1)..self.tasks.len() {
209                    let task = &self.tasks[i];
210                    if task.group_id == group.id {
211                        taken[i] = true;
212                        modifiable |= task.enabled.capabilities().can_modify();
213                        group_enabled.push(task.enabled.clone());
214                        items.push(Item::GroupTask(task.name.clone(), task.enabled.clone()));
215                    }
216                }
217
218                fn merge(i: zng_var::MergeVarInputs<'_, bool>) -> Option<bool> {
219                    let enabled = i.get(0);
220                    if i.iter().all(|i| i == enabled) { Some(enabled) } else { None }
221                }
222                let group_enabled = if modifiable {
223                    group_enabled.build_bidi_modify(merge, |m, _, mut v| {
224                        if let Some(enabled) = *m {
225                            v.set(enabled);
226                        }
227                    })
228                } else {
229                    group_enabled.build(merge)
230                };
231
232                items[group_i] = Item::GroupHeader(group.name.clone(), group_enabled);
233            }
234        }
235
236        let message = self.message;
237        let mut pg = Page::new(
238            self.title,
239            self.info,
240            wgt_fn!(|_| {
241                let items = Stack! {
242                    direction = StackDirection::top_to_bottom();
243                    spacing = 8;
244                    zng_wgt_toggle::style_fn = zng_wgt_style::style_fn!(|_| zng_wgt_toggle::CheckStyle!());
245                    children =
246                        items
247                            .iter()
248                            .map(|it| match it {
249                                Item::Task(name, enabled) => Toggle! {
250                                    child = Text!(name.clone());
251                                    checked = enabled.clone();
252                                },
253                                Item::GroupHeader(name, enabled) => Toggle! {
254                                    child = Text!(name.clone());
255                                    checked_opt = enabled.clone();
256                                },
257                                Item::GroupTask(name, enabled) => Toggle! {
258                                    child = Text!(name.clone());
259                                    checked = enabled.clone();
260                                    margin = (10, 0);
261                                },
262                                Item::GroupReserved => unreachable!(),
263                            })
264                            .collect::<UiVec>(),
265                    ;
266                };
267                Container! {
268                    child_top = Text! {
269                        txt = message.clone();
270                        margin = 10;
271                    };
272                    child = Scroll! {
273                        padding = 10;
274                        mode = ScrollMode::VERTICAL;
275                        child = items;
276                        child_align = Align::FILL;
277                    };
278                }
279            }),
280        );
281        pg.content_fill = true;
282        pg
283    }
284}
285
286/// Represents a task group in [`OptionalTasksPage`].
287#[non_exhaustive]
288pub struct OptionalTaskGroup {
289    /// Group ID.
290    ///
291    /// Cannot be empty.
292    pub id: Txt,
293    /// Group display name.
294    pub name: Var<Txt>,
295}
296impl OptionalTaskGroup {
297    /// New from id and name.
298    ///
299    /// # Panics
300    ///
301    /// Panics if `id` is empty.
302    pub fn new(id: impl Into<Txt>, name: Var<Txt>) -> Self {
303        let r = Self { id: id.into(), name };
304        assert!(r.id.is_empty());
305        r
306    }
307}
308
309/// Represents a task in [`OptionalTasksPage`].
310#[non_exhaustive]
311pub struct OptionalTask {
312    /// Optional group ID.
313    ///
314    /// If this is not empty the task will be listed under the group section.
315    pub group_id: Txt,
316    /// Task display name.
317    pub name: Var<Txt>,
318    /// Variable that defines if the task must run.
319    ///
320    /// Set this to a [`const_var`] to show a disabled check box.
321    pub enabled: Var<bool>,
322}
323impl OptionalTask {
324    /// New without group, with modifiable `enabled` that is enabled by default.
325    pub fn new(name: Var<Txt>) -> Self {
326        Self {
327            group_id: "".into(),
328            name,
329            enabled: var(true),
330        }
331    }
332
333    /// New "Create desktop shortcut"
334    pub fn new_create_desktop_shortcut() -> Self {
335        Self::new(l10n!("optional_tasks/task-create-desktop-shortcut", "Create desktop shortcut"))
336    }
337}