ferroui_dialogs/
managed_storage_provider.rs1use 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#[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 {
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 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 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}