Skip to main content

ferroui_dialogs/
managed_storage_provider.rs

1use crate::internal::{ManagedFileChooserFilterViewModel, ManagedFileChooserViewModel};
2use crate::task_completion_source::TaskCompletionSource;
3use crate::{ManagedFileChooser, ManagedFileChooserOverwritePrompt, ManagedFileDialogOptions};
4use ferroui_base::input::LocalBoxFuture;
5use ferroui_base::interactivity::RoutedEventHandlerToken;
6use ferroui_base::layout::Layoutable;
7use ferroui_base::platform::storage::file_io::{path, BclStorageFile, BclStorageFolder, BclStorageProvider, FileSystemInfo};
8use ferroui_base::platform::storage::{
9    FilePickerFileType, FilePickerOpenOptions, FilePickerSaveOptions, FolderPickerOpenOptions, IStorageFile,
10    IStorageFolder, OpenFilePickerResult, SaveFilePickerResult,
11};
12use ferroui_base::threading::Dispatcher;
13use ferroui_base::{BoxedValue, Ref, WeakRef};
14use ferroui_controls::primitives::FlyoutBase;
15use ferroui_controls::primitives::Popup;
16use ferroui_controls::{ContentControl, Control, Flyout, Panel, PlacementMode, TopLevel, Window, WindowBase};
17use std::cell::{Cell, RefCell};
18use std::io;
19use std::rc::Rc;
20
21/// The storage provider that shows the managed file chooser: in a window,
22/// or in a popup over the content of a top level that has no windows.
23///
24/// Internal upstream; public here for the platform backends.
25///
26/// The top level is held weakly: the provider is usually the storage
27/// provider of that top level, which keeps it.
28#[derive(Clone)]
29pub struct ManagedStorageProvider {
30    parent: Option<WeakRef<TopLevel>>,
31    managed_options: ManagedFileDialogOptions,
32}
33
34impl ManagedStorageProvider {
35    pub fn new(parent: Option<&Ref<TopLevel>>, managed_options: Option<ManagedFileDialogOptions>) -> Self {
36        Self { parent: parent.map(Ref::downgrade), managed_options: managed_options.unwrap_or_default() }
37    }
38
39    fn parent(&self) -> Option<Ref<TopLevel>> {
40        self.parent.as_ref().and_then(WeakRef::upgrade)
41    }
42
43    fn prepare_root(&self, model: &Rc<ManagedFileChooserViewModel>) -> Ref<ContentControl> {
44        let root = match self.managed_options.content_root_factory() {
45            Some(factory) => factory(),
46            None => {
47                if self.parent().is_some_and(|parent| !parent.is::<Window>()) {
48                    ContentControl::new()
49                } else {
50                    Window::new().upcast()
51                }
52            }
53        };
54
55        root.set_content(Some(Control::boxed(ManagedFileChooser::new())));
56        root.set_data_context(Some(model.clone() as BoxedValue));
57
58        root
59    }
60
61    async fn show(&self, model: Rc<ManagedFileChooserViewModel>) -> io::Result<Vec<String>> {
62        let root = self.prepare_root(&model);
63
64        if let Some(window) = root.cast::<Window>() {
65            Ok(self.show_as_window(window, model).await)
66        } else if let Some(parent) = self.parent() {
67            self.show_as_popup(&parent, root, model).await
68        } else {
69            Err(io::Error::other("Managed File Chooser requires existing parent or compatible windowing system."))
70        }
71    }
72
73    async fn show_as_window(&self, window: Ref<Window>, model: Rc<ManagedFileChooserViewModel>) -> Vec<String> {
74        let tcs = TaskCompletionSource::<bool>::new();
75        window.set_title(model.title());
76        {
77            let model = model.clone();
78            let tcs = tcs.clone();
79            window.closed(move || {
80                model.cancel();
81                tcs.try_set_result(true);
82            });
83        }
84
85        let result: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
86
87        // The handlers of the model hold the window weakly: the window holds the model as its
88        // data context.
89        {
90            let result = result.clone();
91            let weak = window.downgrade();
92            model.complete_requested(move |items| {
93                *result.borrow_mut() = items.to_vec();
94                if let Some(window) = weak.upgrade() {
95                    window.close();
96                }
97            });
98        }
99
100        {
101            let result = result.clone();
102            let weak = window.downgrade();
103            model.overwrite_prompt(move |filename| {
104                let filename = filename.to_string();
105                let result = result.clone();
106                let weak = weak.clone();
107                Dispatcher::ui_thread().invoke_async_task_local(move || async move {
108                    let Some(window) = weak.upgrade() else { return };
109                    if Self::show_overwrite_prompt(&filename, &window.clone().upcast()).await {
110                        *result.borrow_mut() = vec![filename];
111                        window.close();
112                    }
113                });
114            });
115        }
116
117        {
118            let weak = window.downgrade();
119            model.cancel_requested(move || {
120                if let Some(window) = weak.upgrade() {
121                    window.close();
122                }
123            });
124        }
125
126        match self.parent().and_then(|parent| parent.cast::<Window>()) {
127            Some(parent) => {
128                // The dialog result is not used: the outcome comes from the model.
129                let _ = window.show_dialog(&parent).await;
130            }
131            None => WindowBase::show(&window),
132        }
133
134        tcs.task().await;
135
136        let result = result.borrow().clone();
137        result
138    }
139
140    async fn show_as_popup(
141        &self,
142        parent: &Ref<TopLevel>,
143        root: Ref<ContentControl>,
144        model: Rc<ManagedFileChooserViewModel>,
145    ) -> io::Result<Vec<String>> {
146        let tcs = TaskCompletionSource::<bool>::new();
147        let root_panel = parent
148            .find_descendant_of_type::<Panel>(false)
149            .ok_or_else(|| io::Error::other("The top level has no panel to host the managed file chooser."))?;
150
151        let popup = Popup::new();
152        popup.set_placement(PlacementMode::Center);
153        popup.set_is_light_dismiss_enabled(false);
154        popup.set_child(root.clone());
155        popup.set_width(Layoutable::width(parent));
156        popup.set_height(Layoutable::height(parent));
157
158        {
159            let model = model.clone();
160            let tcs = tcs.clone();
161            popup.closed(move || {
162                model.cancel();
163                tcs.try_set_result(true);
164            });
165        }
166
167        let result: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
168
169        {
170            let result = result.clone();
171            let weak = popup.downgrade();
172            model.complete_requested(move |items| {
173                *result.borrow_mut() = items.to_vec();
174                if let Some(popup) = weak.upgrade() {
175                    popup.close();
176                }
177            });
178        }
179
180        {
181            let result = result.clone();
182            let weak_popup = popup.downgrade();
183            let weak_root = root.downgrade();
184            model.overwrite_prompt(move |filename| {
185                let filename = filename.to_string();
186                let result = result.clone();
187                let weak_popup = weak_popup.clone();
188                let weak_root = weak_root.clone();
189                Dispatcher::ui_thread().invoke_async_task_local(move || async move {
190                    let Some(root) = weak_root.upgrade() else { return };
191                    if Self::show_overwrite_prompt(&filename, &root).await {
192                        *result.borrow_mut() = vec![filename];
193                        if let Some(popup) = weak_popup.upgrade() {
194                            popup.close();
195                        }
196                    }
197                });
198            });
199        }
200
201        {
202            let weak = popup.downgrade();
203            model.cancel_requested(move || {
204                if let Some(popup) = weak.upgrade() {
205                    popup.close();
206                }
207            });
208        }
209
210        root_panel.children().add(popup.clone());
211
212        // `ParentOnSizeChanged`: unsubscribes itself once the popup is closed, and keeps the
213        // popup the size of the top level.
214        let token: Rc<Cell<Option<RoutedEventHandlerToken>>> = Rc::new(Cell::new(None));
215        {
216            let weak_popup = popup.downgrade();
217            let weak_parent = parent.downgrade();
218            let handler_token = token.clone();
219            token.set(Some(parent.size_changed(move |_, _| {
220                let (Some(popup), Some(parent)) = (weak_popup.upgrade(), weak_parent.upgrade()) else { return };
221                if !popup.is_open() {
222                    if let Some(token) = handler_token.take() {
223                        parent.remove_handler(Control::size_changed_event(), token);
224                    }
225                }
226
227                popup.set_width(Layoutable::width(&parent));
228                popup.set_height(Layoutable::height(&parent));
229            })));
230        }
231
232        popup.open();
233        tcs.task().await;
234
235        root_panel.children().remove(popup.clone());
236        if let Some(token) = token.take() {
237            parent.remove_handler(Control::size_changed_event(), token);
238        }
239
240        let result = result.borrow().clone();
241        Ok(result)
242    }
243
244    async fn show_overwrite_prompt(filename: &str, root: &Ref<ContentControl>) -> bool {
245        let tcs = TaskCompletionSource::<bool>::new();
246        let prompt = ManagedFileChooserOverwritePrompt::new();
247        prompt.set_file_name(path::get_file_name(filename).to_string());
248        {
249            let tcs = tcs.clone();
250            prompt.result(move |r| {
251                tcs.try_set_result(r);
252            });
253        }
254
255        let flyout = Flyout::new();
256        {
257            let tcs = tcs.clone();
258            flyout.closed(move || {
259                tcs.try_set_result(false);
260            });
261        }
262        flyout.set_content(Some(Control::boxed(prompt)));
263        flyout.set_placement(PlacementMode::Center);
264        FlyoutBase::show_at(&flyout, root);
265
266        let prompt_result = tcs.task().await;
267        FlyoutBase::hide(&flyout);
268
269        prompt_result
270    }
271
272    fn try_get_selected_file_type(
273        file_types: Option<&[Rc<FilePickerFileType>]>,
274        selected_file_type: Option<&Rc<ManagedFileChooserFilterViewModel>>,
275    ) -> Option<Rc<FilePickerFileType>> {
276        let file_types = file_types?;
277        let index = selected_file_type?.index();
278        if index >= 0 && (index as usize) < file_types.len() {
279            Some(file_types[index as usize].clone())
280        } else {
281            None
282        }
283    }
284}
285
286impl BclStorageProvider for ManagedStorageProvider {
287    fn can_open(&self) -> bool {
288        true
289    }
290
291    fn open_file_picker_with_result_async(
292        &self,
293        options: FilePickerOpenOptions,
294    ) -> LocalBoxFuture<io::Result<OpenFilePickerResult>> {
295        let this = self.clone();
296        Box::pin(async move {
297            let model = ManagedFileChooserViewModel::new_open_file(&options, this.managed_options.clone());
298            let results = this.show(model.clone()).await?;
299
300            let files = results
301                .iter()
302                .map(|f| BclStorageFile::new(FileSystemInfo::file(f)) as Rc<dyn IStorageFile>)
303                .collect();
304            let file_type = Self::try_get_selected_file_type(options.file_type_filter(), model.selected_filter().as_ref());
305
306            Ok(OpenFilePickerResult { files, selected_file_type: file_type })
307        })
308    }
309
310    fn can_save(&self) -> bool {
311        true
312    }
313
314    fn save_file_picker_with_result_async(
315        &self,
316        options: FilePickerSaveOptions,
317    ) -> LocalBoxFuture<io::Result<SaveFilePickerResult>> {
318        let this = self.clone();
319        Box::pin(async move {
320            let model = ManagedFileChooserViewModel::new_save_file(&options, this.managed_options.clone());
321            let results = this.show(model.clone()).await?;
322
323            let file = results.first().map(|result| BclStorageFile::new(FileSystemInfo::file(result)) as Rc<dyn IStorageFile>);
324            let file_type = Self::try_get_selected_file_type(options.file_type_choices(), model.selected_filter().as_ref());
325
326            Ok(SaveFilePickerResult { file, selected_file_type: file_type })
327        })
328    }
329
330    fn can_pick_folder(&self) -> bool {
331        true
332    }
333
334    fn open_folder_picker_async(
335        &self,
336        options: FolderPickerOpenOptions,
337    ) -> LocalBoxFuture<io::Result<Vec<Rc<dyn IStorageFolder>>>> {
338        let this = self.clone();
339        Box::pin(async move {
340            let model = ManagedFileChooserViewModel::new_open_folder(&options, this.managed_options.clone());
341            let results = this.show(model).await?;
342
343            Ok(results
344                .iter()
345                .map(|f| BclStorageFolder::new(FileSystemInfo::directory(f)) as Rc<dyn IStorageFolder>)
346                .collect())
347        })
348    }
349}