Skip to main content

ferroui_xaml_interactions/storage_provider/
open_file_picker_action.rs

1//! Port of `StorageProvider/OpenFilePickerAction.cs`.
2
3use super::utilities::FileFilterParser;
4use super::{FilePickerEventArgs, PickerActionBase};
5use crate::clipboard::visual_of;
6use crate::StorageFileRef;
7use ferroui_base::platform::storage::FilePickerOpenOptions;
8use ferroui_base::reactive::{Disposable, IDisposable};
9use ferroui_base::utilities::HandlerList;
10use ferroui_base::{
11    ferro_class, ferro_class_info, ferro_properties, instantiate, BoxedValue, FerroObjectImpl, FerroObjectImplExt,
12    FerroProperty, Ref, StyledElementImpl, StyledProperty, Visual,
13};
14use ferroui_xaml_interactivity::StyledElementActionImpl;
15use std::rc::Rc;
16
17/// An action that will open a file picker dialog.
18///
19/// The picked files (`Vec<StorageFileRef>`) are given to the handlers of [`pick`](Self::pick) and,
20/// unless a handler handled them, to the command.
21///
22/// Deviation (DEVIATIONS.md, Behaviors): the `Pick` event is an event of the Rust API (a handler
23/// list), not declared to markup.
24#[repr(C)]
25pub struct OpenFilePickerAction {
26    base: PickerActionBase,
27    pick: HandlerList<dyn Fn(&FilePickerEventArgs)>,
28}
29
30ferro_class! {
31    OpenFilePickerAction: PickerActionBase, virtuals OpenFilePickerActionImpl: StyledElementActionImpl {
32        /// Raises the [`pick`](OpenFilePickerAction::pick) event.
33        fn on_pick(this, args: &FilePickerEventArgs);
34    }
35}
36ferro_class_info!(OpenFilePickerAction { new: OpenFilePickerAction::new });
37ferroui_base::ferro_impl_classes!(OpenFilePickerAction: StyledElementImpl);
38
39ferro_properties! {
40    impl OpenFilePickerAction {
41        /// Identifies the `AllowMultiple` property.
42        pub fn allow_multiple_property() -> StyledProperty<bool> {
43            FerroProperty::register::<OpenFilePickerAction, _>("AllowMultiple", false)
44        }
45
46        /// Identifies the `FileTypeFilter` property.
47        pub fn file_type_filter_property() -> StyledProperty<Option<String>> {
48            FerroProperty::register::<OpenFilePickerAction, _>("FileTypeFilter", None)
49        }
50    }
51}
52
53ferroui_base::ferro_overrides! {
54    impl FerroObjectImpl for OpenFilePickerAction {
55        /// The constructor body: the picked files are passed to the command.
56        fn constructed(this: &Self) {
57            Self::parent_constructed(this);
58            this.set_pass_event_args_to_command(true);
59        }
60    }
61}
62
63impl StyledElementActionImpl for OpenFilePickerAction {
64    /// Executes the action.
65    ///
66    /// Returns the task of the picker operation; false when the sender is not a visual.
67    fn execute(this: &Self, sender: Option<&BoxedValue>, _parameter: Option<&BoxedValue>) -> Option<BoxedValue> {
68        let Some(visual) = visual_of(sender) else {
69            return Some(Rc::new(false));
70        };
71
72        let action: Ref<OpenFilePickerAction> = this.to_ref();
73        let task = this.track_dispatched_picker_operation(move || async move {
74            action.open_file_picker_async(&visual).await;
75        });
76        Some(Rc::new(task))
77    }
78}
79
80impl OpenFilePickerActionImpl for OpenFilePickerAction {
81    fn on_pick(this: &Self, args: &FilePickerEventArgs) {
82        for (_, handler) in this.pick.snapshot().iter() {
83            handler(args);
84        }
85    }
86}
87
88impl OpenFilePickerAction {
89    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
90    pub fn construct() -> Self {
91        Self { base: PickerActionBase::construct(), pick: HandlerList::new() }
92    }
93
94    /// Creates an action.
95    pub fn new() -> Ref<Self> {
96        instantiate(Self::construct())
97    }
98
99    /// Occurs after the file picker successfully returns one or more files. Disposing the returned
100    /// handle removes the handler.
101    pub fn pick(&self, handler: impl Fn(&FilePickerEventArgs) + 'static) -> Rc<dyn IDisposable> {
102        let token = self.pick.add(Rc::new(handler));
103        let this = self.to_ref().downgrade();
104        Disposable::create(move || {
105            if let Some(this) = this.upgrade() {
106                this.pick.remove(token);
107            }
108        })
109    }
110
111    /// Gets or sets an option indicating whether the open picker allows users to select multiple
112    /// files. This is a property of the object model.
113    pub fn allow_multiple(&self) -> bool {
114        self.get_value(Self::allow_multiple_property())
115    }
116
117    pub fn set_allow_multiple(&self, value: bool) {
118        self.set_value(Self::allow_multiple_property(), value)
119    }
120
121    /// Gets or sets the file types that the file open picker displays, as a filter
122    /// (`Description|*.ext;*.other|..`). This is a property of the object model.
123    pub fn file_type_filter(&self) -> Option<String> {
124        self.get_value(Self::file_type_filter_property())
125    }
126
127    pub fn set_file_type_filter(&self, value: Option<String>) {
128        self.set_value(Self::file_type_filter_property(), value)
129    }
130
131    async fn open_file_picker_async(&self, visual: &Visual) {
132        let command = self.command();
133        if !self.is_enabled() || (command.is_none() && self.pick.is_empty()) {
134            return;
135        }
136
137        let Some(storage_provider) = self.resolve_storage_provider(Some(visual)) else {
138            return;
139        };
140
141        let suggested_start_location = self.resolve_suggested_start_location(Some(storage_provider.clone())).await;
142
143        let mut options = FilePickerOpenOptions::new();
144        options.set_title(self.title());
145        options.set_suggested_start_location(suggested_start_location);
146        options.set_suggested_file_name(self.suggested_file_name());
147        options.set_allow_multiple(self.allow_multiple());
148        options.set_file_type_filter(
149            self.file_type_filter().and_then(|filter| FileFilterParser::convert_to_file_picker_file_type(&filter)),
150        );
151
152        // A failure of the picker fails the operation.
153        let files = storage_provider.open_file_picker_async(options).await.unwrap_or_else(|error| panic!("{error}"));
154
155        if files.is_empty() {
156            return;
157        }
158
159        let files: Vec<StorageFileRef> = files.into_iter().map(StorageFileRef::new).collect();
160        let event_args = FilePickerEventArgs::new(files.clone());
161        self.on_pick(&event_args);
162        let Some(command) = command.filter(|_| !event_args.handled()) else {
163            return;
164        };
165
166        let resolved_parameter = self.resolve_parameter(Some(&(Rc::new(files) as BoxedValue)));
167
168        if !command.can_execute(resolved_parameter.as_ref()) {
169            return;
170        }
171
172        command.execute(resolved_parameter.as_ref());
173    }
174}