Skip to main content

ferroui_controls/date_time_pickers/
time_picker.rs

1use super::{PickerPresenterBase, TimePickerPresenter, TimePickerSelectedValueChangedEventArgs};
2use crate::primitives::popup_positioning::{PopupAnchor, PopupGravity, PopupPositionerConstraintAdjustment};
3use crate::primitives::{Popup, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl, TemplatedControlImplExt};
4use crate::shapes::Rectangle;
5use crate::utils::TimeUtils;
6use crate::{
7    Border, Button, ColumnDefinition, ColumnDefinitions, ContentControl, ControlImpl, Grid, GridLength, PlacementMode,
8    TextBlock,
9};
10use ferroui_base::animation::TimeSpan;
11use ferroui_base::data::BindingMode;
12use ferroui_base::input::InputElementImpl;
13use ferroui_base::interactivity::{InteractiveImpl, RoutedEventHandlerToken};
14use ferroui_base::layout::{LayoutableImpl, VerticalAlignment};
15use ferroui_base::reactive::{Disposable, IDisposable};
16use ferroui_base::utilities::{CultureInfo, DateTime, HandlerList};
17use ferroui_base::{
18    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, FerroObject, FerroObjectImpl, FerroObjectImplExt,
19    FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty, StyledPropertyOptions,
20    VisualImpl,
21};
22use std::cell::{Cell, RefCell};
23use std::rc::Rc;
24
25const PC_HAS_NO_TIME: &str = ":hasnotime";
26
27/// A control to allow the user to select a time.
28#[repr(C)]
29pub struct TimePicker {
30    base: TemplatedControl,
31
32    // Template Items
33    presenter: RefCell<Option<Ref<TimePickerPresenter>>>,
34    flyout_button: RefCell<Option<Ref<Button>>>,
35    first_picker_host: RefCell<Option<Ref<Border>>>,
36    second_picker_host: RefCell<Option<Ref<Border>>>,
37    third_picker_host: RefCell<Option<Ref<Border>>>,
38    fourth_picker_host: RefCell<Option<Ref<Border>>>,
39    hour_text: RefCell<Option<Ref<TextBlock>>>,
40    minute_text: RefCell<Option<Ref<TextBlock>>>,
41    second_text: RefCell<Option<Ref<TextBlock>>>,
42    period_text: RefCell<Option<Ref<TextBlock>>>,
43    first_splitter: RefCell<Option<Ref<Rectangle>>>,
44    second_splitter: RefCell<Option<Ref<Rectangle>>>,
45    third_splitter: RefCell<Option<Ref<Rectangle>>>,
46    content_grid: RefCell<Option<Ref<Grid>>>,
47    popup: RefCell<Option<Ref<Popup>>>,
48
49    /// The click handler of the flyout button, while it is added.
50    flyout_button_click: Cell<Option<RoutedEventHandlerToken>>,
51    /// The subscriptions to the confirmed and dismissed events of the
52    /// presenter.
53    presenter_subscriptions: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
54
55    selected_time_changed: HandlerList<dyn Fn(&TimePickerSelectedValueChangedEventArgs)>,
56}
57
58ferro_class! {
59    TimePicker: TemplatedControl, virtuals TimePickerImpl: TemplatedControlImpl {
60        /// Raises the `SelectedTimeChanged` event.
61        fn on_selected_time_changed(this, old_time: Option<TimeSpan>, new_time: Option<TimeSpan>);
62    }
63}
64
65ferro_class_info!(TimePicker {
66    new: TimePicker::new,
67    markup: {
68        attributes: [
69            TemplatePart("PART_FirstColumnDivider", type(Ref<Rectangle>)),
70            TemplatePart("PART_FirstPickerHost", type(Ref<Border>)),
71            TemplatePart("PART_FlyoutButton", type(Ref<Button>)),
72            TemplatePart("PART_FlyoutButtonContentGrid", type(Ref<Grid>)),
73            TemplatePart("PART_HourTextBlock", type(Ref<TextBlock>)),
74            TemplatePart("PART_MinuteTextBlock", type(Ref<TextBlock>)),
75            TemplatePart("PART_SecondTextBlock", type(Ref<TextBlock>)),
76            TemplatePart("PART_PeriodTextBlock", type(Ref<TextBlock>)),
77            TemplatePart("PART_PickerPresenter", type(Ref<TimePickerPresenter>)),
78            TemplatePart("PART_Popup", type(Ref<Popup>)),
79            TemplatePart("PART_SecondColumnDivider", type(Ref<Rectangle>)),
80            TemplatePart("PART_SecondPickerHost", type(Ref<Border>)),
81            TemplatePart("PART_ThirdColumnDivider", type(Ref<Rectangle>)),
82            TemplatePart("PART_ThirdPickerHost", type(Ref<Border>)),
83            TemplatePart("PART_FourthPickerHost", type(Ref<Border>)),
84            PseudoClasses(":hasnotime"),
85        ],
86    },
87});
88
89ferro_impl_classes!(
90    TimePicker: StyledElementImpl,
91    VisualImpl,
92    LayoutableImpl,
93    InteractiveImpl,
94    InputElementImpl
95);
96
97impl FerroObjectImpl for TimePicker {
98    fn constructed(this: &Self) {
99        Self::parent_constructed(this);
100
101        this.pseudo_classes().set(PC_HAS_NO_TIME, true);
102
103        let date_time_format = CultureInfo::current_culture().date_time_format();
104        let time_pattern = date_time_format.short_time_pattern();
105        if time_pattern.contains('H') {
106            this.set_current_value(Self::clock_identifier_property(), "24HourClock".to_string());
107        }
108    }
109
110    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
111        Self::parent_on_property_changed(this, change);
112
113        let property = change.property();
114        if property == Self::minute_increment_property().as_property() {
115            this.set_selected_time_text();
116        } else if property == Self::second_increment_property().as_property() {
117            this.set_selected_time_text();
118        } else if property == Self::clock_identifier_property().as_property() {
119            this.set_grid();
120            this.set_selected_time_text();
121        } else if property == Self::use_seconds_property().as_property() {
122            this.set_grid();
123            this.set_selected_time_text();
124        } else if property == Self::selected_time_property().as_property() {
125            let (old_value, new_value) = change.get_old_and_new_value::<Option<TimeSpan>>();
126            this.on_selected_time_changed(old_value, new_value);
127            this.set_selected_time_text();
128        }
129    }
130}
131
132impl TemplatedControlImpl for TimePicker {
133    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
134        let flyout_button = this.flyout_button.borrow().clone();
135        if let (Some(flyout_button), Some(token)) = (flyout_button, this.flyout_button_click.take()) {
136            flyout_button.remove_handler(Button::click_event(), token);
137        }
138
139        if let Some((confirmed, dismissed)) = this.presenter_subscriptions.take() {
140            confirmed.dispose();
141            dismissed.dispose();
142        }
143        Self::parent_on_apply_template(this, e);
144
145        let name_scope = e.name_scope();
146        let flyout_button = name_scope.find_as::<Button>("PART_FlyoutButton");
147        *this.flyout_button.borrow_mut() = flyout_button.clone();
148
149        *this.first_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_FirstPickerHost");
150        *this.second_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_SecondPickerHost");
151        *this.third_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_ThirdPickerHost");
152        *this.fourth_picker_host.borrow_mut() = name_scope.find_as::<Border>("PART_FourthPickerHost");
153
154        *this.hour_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_HourTextBlock");
155        *this.minute_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_MinuteTextBlock");
156        *this.second_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_SecondTextBlock");
157        *this.period_text.borrow_mut() = name_scope.find_as::<TextBlock>("PART_PeriodTextBlock");
158
159        *this.first_splitter.borrow_mut() = name_scope.find_as::<Rectangle>("PART_FirstColumnDivider");
160        *this.second_splitter.borrow_mut() = name_scope.find_as::<Rectangle>("PART_SecondColumnDivider");
161        *this.third_splitter.borrow_mut() = name_scope.find_as::<Rectangle>("PART_ThirdColumnDivider");
162
163        *this.content_grid.borrow_mut() = name_scope.find_as::<Grid>("PART_FlyoutButtonContentGrid");
164
165        *this.popup.borrow_mut() = name_scope.find_as::<Popup>("PART_Popup");
166        let presenter = name_scope.find_as::<TimePickerPresenter>("PART_PickerPresenter");
167        *this.presenter.borrow_mut() = presenter.clone();
168
169        if let Some(flyout_button) = flyout_button {
170            let weak = this.to_ref().downgrade();
171            let token = flyout_button.click(move |_, _| {
172                if let Some(this) = weak.upgrade() {
173                    this.on_flyout_button_clicked();
174                }
175            });
176            this.flyout_button_click.set(Some(token));
177        }
178
179        this.set_grid();
180        this.set_selected_time_text();
181
182        if let Some(presenter) = presenter {
183            let weak = this.to_ref().downgrade();
184            let confirmed = PickerPresenterBase::confirmed(&presenter, move || {
185                if let Some(this) = weak.upgrade() {
186                    this.on_confirmed();
187                }
188            });
189            let weak = this.to_ref().downgrade();
190            let dismissed = PickerPresenterBase::dismissed(&presenter, move || {
191                if let Some(this) = weak.upgrade() {
192                    this.on_dismiss_picker();
193                }
194            });
195            *this.presenter_subscriptions.borrow_mut() = Some((confirmed, dismissed));
196
197            let bind = |target: &'static FerroProperty, source: &'static FerroProperty| {
198                presenter.bind_indexer(&target.bind(), &this.indexer(&source.bind()));
199            };
200
201            bind(
202                TimePickerPresenter::minute_increment_property().as_property(),
203                Self::minute_increment_property().as_property(),
204            );
205            bind(
206                TimePickerPresenter::second_increment_property().as_property(),
207                Self::second_increment_property().as_property(),
208            );
209            bind(
210                TimePickerPresenter::clock_identifier_property().as_property(),
211                Self::clock_identifier_property().as_property(),
212            );
213            bind(TimePickerPresenter::use_seconds_property().as_property(), Self::use_seconds_property().as_property());
214        }
215    }
216}
217
218impl ControlImpl for TimePicker {
219    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
220        crate::automation::peers::TimePickerAutomationPeer::new(this).upcast()
221    }
222}
223
224impl TimePickerImpl for TimePicker {
225    fn on_selected_time_changed(this: &Self, old_time: Option<TimeSpan>, new_time: Option<TimeSpan>) {
226        let e = TimePickerSelectedValueChangedEventArgs::new(old_time, new_time);
227        for (_, handler) in this.selected_time_changed.snapshot().iter() {
228            handler(&e);
229        }
230    }
231}
232
233ferro_properties! {
234    impl TimePicker {
235        /// Defines the `MinuteIncrement` property.
236        pub fn minute_increment_property() -> StyledProperty<i32> {
237            FerroProperty::register_with::<TimePicker, _>(
238                "MinuteIncrement",
239                StyledPropertyOptions::new(1).coerce(TimePicker::coerce_minute_increment),
240            )
241        }
242
243        /// Defines the `SecondIncrement` property.
244        pub fn second_increment_property() -> StyledProperty<i32> {
245            FerroProperty::register_with::<TimePicker, _>(
246                "SecondIncrement",
247                StyledPropertyOptions::new(1).coerce(TimePicker::coerce_second_increment),
248            )
249        }
250
251        /// Defines the `ClockIdentifier` property.
252        pub fn clock_identifier_property() -> StyledProperty<String> {
253            FerroProperty::register_with::<TimePicker, _>(
254                "ClockIdentifier",
255                StyledPropertyOptions::new("12HourClock".to_string()).coerce(TimePicker::coerce_clock_identifier),
256            )
257        }
258
259        /// Defines the `VerticalContentAlignment` property.
260        pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
261            ContentControl::vertical_content_alignment_property().add_owner::<TimePicker>()
262        }
263
264        /// Defines the `UseSeconds` property.
265        pub fn use_seconds_property() -> StyledProperty<bool> {
266            FerroProperty::register_with::<TimePicker, _>(
267                "UseSeconds",
268                StyledPropertyOptions::new(false).coerce(TimePicker::coerce_use_seconds),
269            )
270        }
271
272        /// Defines the `SelectedTime` property.
273        pub fn selected_time_property() -> StyledProperty<Option<TimeSpan>> {
274            FerroProperty::register_with::<TimePicker, _>(
275                "SelectedTime",
276                StyledPropertyOptions::new(None)
277                    .default_binding_mode(BindingMode::TwoWay)
278                    .enable_data_validation(true),
279            )
280        }
281    }
282}
283
284impl TimePicker {
285    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
286    pub fn construct() -> Self {
287        Self {
288            base: TemplatedControl::construct(),
289            presenter: RefCell::new(None),
290            flyout_button: RefCell::new(None),
291            first_picker_host: RefCell::new(None),
292            second_picker_host: RefCell::new(None),
293            third_picker_host: RefCell::new(None),
294            fourth_picker_host: RefCell::new(None),
295            hour_text: RefCell::new(None),
296            minute_text: RefCell::new(None),
297            second_text: RefCell::new(None),
298            period_text: RefCell::new(None),
299            first_splitter: RefCell::new(None),
300            second_splitter: RefCell::new(None),
301            third_splitter: RefCell::new(None),
302            content_grid: RefCell::new(None),
303            popup: RefCell::new(None),
304            flyout_button_click: Cell::new(None),
305            presenter_subscriptions: RefCell::new(None),
306            selected_time_changed: HandlerList::new(),
307        }
308    }
309
310    pub fn new() -> Ref<Self> {
311        instantiate(Self::construct())
312    }
313
314    /// The vertical alignment of the content within the control.
315    pub fn vertical_content_alignment(&self) -> VerticalAlignment {
316        self.get_value(Self::vertical_content_alignment_property())
317    }
318
319    pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
320        self.set_value(Self::vertical_content_alignment_property(), value)
321    }
322
323    /// The minute increment in the picker.
324    pub fn minute_increment(&self) -> i32 {
325        self.get_value(Self::minute_increment_property())
326    }
327
328    pub fn set_minute_increment(&self, value: i32) {
329        self.set_value(Self::minute_increment_property(), value)
330    }
331
332    fn coerce_minute_increment(_sender: &FerroObject, value: i32) -> i32 {
333        if !(1..=59).contains(&value) {
334            panic!("1 >= MinuteIncrement <= 59");
335        }
336
337        value
338    }
339
340    /// The second increment in the picker.
341    pub fn second_increment(&self) -> i32 {
342        self.get_value(Self::second_increment_property())
343    }
344
345    pub fn set_second_increment(&self, value: i32) {
346        self.set_value(Self::second_increment_property(), value)
347    }
348
349    fn coerce_second_increment(_sender: &FerroObject, value: i32) -> i32 {
350        if !(1..=59).contains(&value) {
351            panic!("1 >= SecondIncrement <= 59");
352        }
353
354        value
355    }
356
357    /// The clock identifier, either `12HourClock` or `24HourClock`.
358    pub fn clock_identifier(&self) -> String {
359        self.get_value(Self::clock_identifier_property())
360    }
361
362    pub fn set_clock_identifier(&self, value: &str) {
363        self.set_value(Self::clock_identifier_property(), value.to_string())
364    }
365
366    fn coerce_clock_identifier(_sender: &FerroObject, value: String) -> String {
367        if !(value.is_empty() || value == "12HourClock" || value == "24HourClock") {
368            panic!("Invalid ClockIdentifier");
369        }
370
371        value
372    }
373
374    /// The use seconds switch.
375    pub fn use_seconds(&self) -> bool {
376        self.get_value(Self::use_seconds_property())
377    }
378
379    pub fn set_use_seconds(&self, value: bool) {
380        self.set_value(Self::use_seconds_property(), value)
381    }
382
383    fn coerce_use_seconds(_sender: &FerroObject, value: bool) -> bool {
384        // Upstream rejects a value that is neither true nor false, which no
385        // value is.
386        value
387    }
388
389    /// The selected time; can be `None`.
390    pub fn selected_time(&self) -> Option<TimeSpan> {
391        self.get_value(Self::selected_time_property())
392    }
393
394    pub fn set_selected_time(&self, value: Option<TimeSpan>) {
395        self.set_value(Self::selected_time_property(), value)
396    }
397
398    /// Raised when the selected time changes. Disposing the returned handle
399    /// removes the handler.
400    pub fn selected_time_changed(
401        &self,
402        handler: impl Fn(&TimePickerSelectedValueChangedEventArgs) + 'static,
403    ) -> Rc<dyn IDisposable> {
404        let token = self.selected_time_changed.add(Rc::new(handler));
405        let weak = self.to_ref().downgrade();
406        Disposable::create(move || {
407            if let Some(this) = weak.upgrade() {
408                this.selected_time_changed.remove(token);
409            }
410        })
411    }
412
413    fn set_grid(&self) {
414        let Some(content_grid) = self.content_grid.borrow().clone() else {
415            return;
416        };
417
418        let first_picker_host = self.first_picker_host.borrow().clone();
419        let second_picker_host = self.second_picker_host.borrow().clone();
420        let third_picker_host = self.third_picker_host.borrow().clone();
421        let fourth_picker_host = self.fourth_picker_host.borrow().clone();
422        let first_splitter = self.first_splitter.borrow().clone();
423        let second_splitter = self.second_splitter.borrow().clone();
424        let third_splitter = self.third_splitter.borrow().clone();
425
426        let use_24_hour_clock = self.clock_identifier() == "24HourClock";
427        let use_seconds = self.use_seconds();
428        let seconds_parts = match (self.second_text.borrow().is_some(), &fourth_picker_host, &third_splitter) {
429            (true, Some(fourth_picker_host), Some(third_splitter)) => Some((fourth_picker_host, third_splitter)),
430            _ => None,
431        };
432        let can_use_seconds = seconds_parts.is_some();
433
434        let columns_d = ColumnDefinitions::new();
435        columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
436        columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
437        columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
438        if can_use_seconds && use_seconds {
439            columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
440            columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
441        }
442        if !use_24_hour_clock {
443            columns_d.add(ColumnDefinition::with_width(GridLength::AUTO));
444            columns_d.add(ColumnDefinition::with_width(GridLength::STAR));
445        }
446
447        content_grid.set_column_definitions(columns_d);
448
449        let third_picker_host = third_picker_host.expect("the template has a third picker host");
450        let second_splitter = second_splitter.expect("the template has a second column divider");
451
452        if let Some((fourth_picker_host, third_splitter)) = seconds_parts {
453            third_picker_host.set_is_visible(use_seconds);
454            second_splitter.set_is_visible(use_seconds);
455            fourth_picker_host.set_is_visible(!use_24_hour_clock);
456            third_splitter.set_is_visible(!use_24_hour_clock);
457        } else {
458            third_picker_host.set_is_visible(!use_24_hour_clock);
459            second_splitter.set_is_visible(!use_24_hour_clock);
460        }
461
462        Grid::set_column(&first_picker_host.expect("the template has a first picker host"), 0);
463        Grid::set_column(&second_picker_host.expect("the template has a second picker host"), 2);
464        let first_splitter = first_splitter.expect("the template has a first column divider");
465
466        if let Some((fourth_picker_host, third_splitter)) = seconds_parts {
467            let am_pm_column = if use_seconds { 6 } else { 4 };
468            Grid::set_column(&third_picker_host, if use_seconds { 4 } else { 0 });
469            Grid::set_column(fourth_picker_host, if use_24_hour_clock { 0 } else { am_pm_column });
470            Grid::set_column(&first_splitter, 1);
471            Grid::set_column(&second_splitter, if use_seconds { 3 } else { 0 });
472            Grid::set_column(third_splitter, if use_24_hour_clock { 0 } else { am_pm_column - 1 });
473        } else {
474            Grid::set_column(&third_picker_host, if use_24_hour_clock { 0 } else { 4 });
475            Grid::set_column(&first_splitter, 1);
476            Grid::set_column(&second_splitter, if use_24_hour_clock { 0 } else { 3 });
477        }
478    }
479
480    fn set_selected_time_text(&self) {
481        let (Some(hour_text), Some(minute_text), Some(period_text)) =
482            (self.hour_text.borrow().clone(), self.minute_text.borrow().clone(), self.period_text.borrow().clone())
483        else {
484            return;
485        };
486        let second_text = self.second_text.borrow().clone();
487
488        if let Some(time) = self.selected_time() {
489            let mut new_time = time;
490
491            if self.clock_identifier() == "12HourClock" {
492                let hr = new_time.hours();
493                let hr = if hr > 12 {
494                    hr - 12
495                } else if hr == 0 {
496                    12
497                } else {
498                    hr
499                };
500                new_time = TimeSpan::from_hms(hr, new_time.minutes(), new_time.seconds());
501            }
502
503            hour_text.set_text(Some(&new_time.to_string_format("%h")));
504            minute_text.set_text(Some(&new_time.to_string_format("mm")));
505            if let Some(second_text) = &second_text {
506                second_text.set_text(Some(&new_time.to_string_format("ss")));
507            }
508
509            self.pseudo_classes().set(PC_HAS_NO_TIME, false);
510
511            period_text.set_text(Some(&if time.hours() >= 12 {
512                TimeUtils::get_pm_designator()
513            } else {
514                TimeUtils::get_am_designator()
515            }));
516        } else {
517            // By clearing local value, we reset text property to the value from the template.
518            hour_text.clear_value(TextBlock::text_property());
519            minute_text.clear_value(TextBlock::text_property());
520            if let Some(second_text) = &second_text {
521                second_text.clear_value(TextBlock::text_property());
522            }
523
524            self.pseudo_classes().set(PC_HAS_NO_TIME, true);
525
526            period_text.set_text(Some(&if DateTime::now().hour() >= 12 {
527                TimeUtils::get_pm_designator()
528            } else {
529                TimeUtils::get_am_designator()
530            }));
531        }
532    }
533
534    fn on_flyout_button_clicked(&self) {
535        let Some(presenter) = self.presenter.borrow().clone() else {
536            panic!("No DatePickerPresenter found.");
537        };
538        let Some(popup) = self.popup.borrow().clone() else {
539            panic!("No Popup found.");
540        };
541
542        presenter.set_time(self.selected_time().unwrap_or_else(|| DateTime::now().time_of_day()));
543
544        popup.set_placement(PlacementMode::AnchorAndGravity);
545        popup.set_placement_anchor(PopupAnchor::BOTTOM);
546        popup.set_placement_gravity(PopupGravity::BOTTOM);
547        popup.set_placement_constraint_adjustment(PopupPositionerConstraintAdjustment::SLIDE_Y);
548        popup.set_is_open(true);
549
550        // Overlay popup hosts won't get measured until the next layout pass, but we need the
551        // template to be applied to the presenter now. Detect this case and force a layout pass.
552        if !presenter.is_measure_valid() {
553            if let Some(layout_manager) = self.get_layout_manager() {
554                layout_manager.execute_initial_layout_pass();
555            }
556        }
557
558        let delta_y = presenter.get_offset_for_popup();
559
560        // The extra 5 px I think is related to default popup placement behavior
561        popup.set_vertical_offset(delta_y + 5.0);
562    }
563
564    fn on_dismiss_picker(&self) {
565        let popup = self.popup.borrow().clone().expect("the template has a popup");
566        popup.close();
567        self.focus();
568    }
569
570    fn on_confirmed(&self) {
571        let popup = self.popup.borrow().clone().expect("the template has a popup");
572        popup.close();
573        let presenter = self.presenter.borrow().clone().expect("the template has a presenter");
574        self.set_current_value(Self::selected_time_property(), Some(presenter.time()));
575    }
576
577    /// Clears the selected time.
578    pub fn clear(&self) {
579        self.set_current_value(Self::selected_time_property(), None);
580    }
581}