Skip to main content

ferroui_dialogs/
managed_file_chooser.rs

1use crate::internal::{ManagedFileChooserItemType, ManagedFileChooserItemViewModel, ManagedFileChooserViewModel};
2use ferroui_base::input::{InputElement, InputElementImpl, PointerPressedEventArgs};
3use ferroui_base::interactivity::{InteractiveImpl, RoutingStrategies};
4use ferroui_base::layout::LayoutableImpl;
5use ferroui_base::metadata::from_markup_value;
6use ferroui_base::threading::{DispatcherPriority, DispatcherTimer};
7use ferroui_base::{
8    ferro_class, ferro_class_info, ferro_impl_classes, instantiate, FerroObjectImpl, FerroObjectImplExt, Ref,
9    StyledElement, StyledElementImpl, StyledElementImplExt, VisualImpl,
10};
11use ferroui_controls::primitives::{TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl};
12use ferroui_controls::{Control, ControlImpl, ListBox};
13use std::cell::RefCell;
14use std::rc::Rc;
15use std::time::Duration;
16
17/// The managed file chooser: the quick links, the entries of the current
18/// folder and the controls of the file name, the filters and the buttons,
19/// over a [`ManagedFileChooserViewModel`] as its data context.
20#[repr(C)]
21pub struct ManagedFileChooser {
22    base: TemplatedControl,
23    quick_links_root: RefCell<Option<Ref<Control>>>,
24    files_view: RefCell<Option<Ref<ListBox>>>,
25}
26
27ferro_class!(ManagedFileChooser: TemplatedControl);
28ferro_impl_classes!(ManagedFileChooser: VisualImpl, LayoutableImpl, InteractiveImpl, InputElementImpl, ControlImpl);
29ferro_class_info!(ManagedFileChooser {
30    new: ManagedFileChooser::new,
31    markup: {
32        attributes: [
33            TemplatePart("PART_QuickLinks", type(Ref<Control>), IsRequired = true),
34            TemplatePart("PART_Files", type(Ref<ListBox>), IsRequired = true),
35        ],
36    },
37});
38
39/// Whether `logical` is a (strict) logical ancestor of `target`
40/// (`LogicalExtensions.IsLogicalAncestorOf`).
41fn is_logical_ancestor_of(logical: &StyledElement, target: Option<&StyledElement>) -> bool {
42    let mut current = target.and_then(|target| target.parent());
43
44    while let Some(parent) = current {
45        if std::ptr::eq::<StyledElement>(&*parent, logical) {
46            return true;
47        }
48        current = parent.parent();
49    }
50
51    false
52}
53
54impl FerroObjectImpl for ManagedFileChooser {
55    fn constructed(this: &Self) {
56        Self::parent_constructed(this);
57
58        let weak = this.to_ref().downgrade();
59        this.add_handler_with(
60            InputElement::pointer_pressed_event(),
61            move |_, e: &PointerPressedEventArgs| {
62                if let Some(this) = weak.upgrade() {
63                    this.on_pointer_pressed(e);
64                }
65            },
66            RoutingStrategies::TUNNEL,
67            false,
68        );
69    }
70}
71
72impl StyledElementImpl for ManagedFileChooser {
73    fn on_data_context_changed(this: &Self) {
74        Self::parent_on_data_context_changed(this);
75
76        let Some(model) = this.model() else {
77            return;
78        };
79
80        let Some(preselected) = model.selected_items().try_get(0) else {
81            return;
82        };
83
84        // Let everything to settle down and scroll to selected item
85        let weak = this.to_ref().downgrade();
86        DispatcherTimer::run_once(
87            move || {
88                let Some(this) = weak.upgrade() else { return };
89
90                if Some(&preselected) != model.selected_items().try_get(0).as_ref() {
91                    return;
92                }
93
94                // Workaround for ListBox bug, scroll to the previous file
95                let index_of_preselected =
96                    model.items().to_vec().iter().position(|item| Some(item.clone()) == from_markup_value(&preselected));
97
98                let files_view = this.files_view.borrow().clone();
99                if let (Some(files_view), Some(index_of_preselected)) = (files_view, index_of_preselected) {
100                    if index_of_preselected > 1 {
101                        files_view.scroll_into_view(index_of_preselected as i32 - 1);
102                    }
103                }
104            },
105            Duration::from_millis(100),
106            DispatcherPriority::DEFAULT,
107        );
108    }
109}
110
111impl TemplatedControlImpl for ManagedFileChooser {
112    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
113        *this.quick_links_root.borrow_mut() = Some(e.name_scope().get_as::<Control>("PART_QuickLinks"));
114        *this.files_view.borrow_mut() = Some(e.name_scope().get_as::<ListBox>("PART_Files"));
115    }
116}
117
118impl ManagedFileChooser {
119    pub fn construct() -> Self {
120        Self { base: TemplatedControl::construct(), quick_links_root: RefCell::new(None), files_view: RefCell::new(None) }
121    }
122
123    pub fn new() -> Ref<Self> {
124        instantiate(Self::construct())
125    }
126
127    fn model(&self) -> Option<Rc<ManagedFileChooserViewModel>> {
128        from_markup_value::<Rc<ManagedFileChooserViewModel>>(&self.data_context())
129    }
130
131    fn on_pointer_pressed(&self, e: &PointerPressedEventArgs) {
132        let source = e.source();
133        let source_element = source.as_ref().and_then(|source| source.cast::<StyledElement>());
134        let Some(model) = source_element
135            .as_ref()
136            .and_then(|element| from_markup_value::<Rc<ManagedFileChooserItemViewModel>>(&element.data_context()))
137        else {
138            return;
139        };
140
141        let quick_links_root = self.quick_links_root.borrow().clone();
142        if let Some(quick_links_root) = quick_links_root {
143            let source_control = source.as_ref().and_then(|source| source.cast::<Control>());
144            let is_quick_link = is_logical_ancestor_of(&quick_links_root, source_control.as_deref().map(|c| -> &StyledElement { c }));
145
146            if e.click_count() == 2 || is_quick_link {
147                if model.item_type() == ManagedFileChooserItemType::File {
148                    if let Some(chooser) = self.model() {
149                        chooser.select_single_file(&model);
150                    }
151                } else if let Some(chooser) = self.model() {
152                    chooser.navigate(model.path().as_deref(), None);
153                }
154
155                e.set_handled(true);
156            }
157        }
158    }
159}