relm4_components/
open_dialog.rs

1//! Reusable and easily configurable open dialog component.
2//!
3//! **[Example implementation](https://github.com/Relm4/Relm4/blob/main/relm4-components/examples/file_dialogs.rs)**
4use gtk::prelude::{Cast, FileChooserExt, FileExt, ListModelExt, NativeDialogExt};
5use relm4::{ComponentParts, ComponentSender, SimpleComponent, gtk};
6
7use std::{fmt::Debug, marker::PhantomData, path::PathBuf};
8
9/// A component that prompts the user to choose a file.
10///
11/// The user would be able to select a single file. If you'd like to select multiple, use [`OpenDialogMulti`].
12pub type OpenDialog = OpenDialogInner<SingleSelection>;
13
14/// A component that prompts the user to choose a file.
15///
16/// The user would be able to select multiple files. If you'd like to select just one, use [`OpenDialog`].
17pub type OpenDialogMulti = OpenDialogInner<MultiSelection>;
18
19/// Type of selection used for the open dialog.
20pub trait Select: Debug {
21    /// Output of the selection.
22    type Selection: Debug;
23    /// Whether to select multiple files inside the dialog.
24    const SELECT_MULTIPLE: bool;
25    /// Construct selection from the file chooser.
26    fn select(dialog: &gtk::FileChooserNative) -> Self::Selection;
27}
28
29/// A type of selection where only one file can be chosen at a time.
30#[derive(Debug)]
31pub struct SingleSelection;
32
33impl Select for SingleSelection {
34    type Selection = PathBuf;
35    const SELECT_MULTIPLE: bool = false;
36    fn select(dialog: &gtk::FileChooserNative) -> Self::Selection {
37        dialog
38            .file()
39            .expect("No file selected")
40            .path()
41            .expect("No path")
42    }
43}
44
45/// A type of selection where multiple types can be chosen at a time.
46#[derive(Debug)]
47pub struct MultiSelection;
48impl Select for MultiSelection {
49    type Selection = Vec<PathBuf>;
50    const SELECT_MULTIPLE: bool = true;
51    fn select(dialog: &gtk::FileChooserNative) -> Self::Selection {
52        let list_model = dialog.files();
53        (0..list_model.n_items())
54            .filter_map(|index| list_model.item(index))
55            .filter_map(|obj| obj.downcast::<gtk::gio::File>().ok())
56            .filter_map(|file| file.path())
57            .collect()
58    }
59}
60
61#[derive(Clone, Debug)]
62/// Configuration for the open dialog component
63pub struct OpenDialogSettings {
64    /// Select folders instead of files
65    pub folder_mode: bool,
66    /// Label for cancel button
67    pub cancel_label: String,
68    /// Label for accept button
69    pub accept_label: String,
70    /// Allow or disallow creating folders
71    pub create_folders: bool,
72    /// Freeze other windows while the dialog is open
73    pub is_modal: bool,
74    /// Filter for MIME types or other patterns
75    pub filters: Vec<gtk::FileFilter>,
76}
77
78impl Default for OpenDialogSettings {
79    fn default() -> Self {
80        OpenDialogSettings {
81            folder_mode: false,
82            accept_label: String::from("Open"),
83            cancel_label: String::from("Cancel"),
84            create_folders: true,
85            is_modal: true,
86            filters: Vec::new(),
87        }
88    }
89}
90
91#[derive(Debug)]
92/// Model for the open dialog component
93pub struct OpenDialogInner<S: Select> {
94    visible: bool,
95    _phantom: PhantomData<S>,
96}
97
98/// Messages that can be sent to the open dialog component
99#[derive(Debug, Clone)]
100pub enum OpenDialogMsg {
101    /// Show the dialog
102    Open,
103    #[doc(hidden)]
104    Hide,
105}
106
107/// Messages that can be sent from the open dialog component
108#[derive(Debug, Clone)]
109pub enum OpenDialogResponse<S: Select> {
110    /// User clicked accept button.
111    Accept(S::Selection),
112    /// User clicked cancel button.
113    Cancel,
114}
115
116/// Widgets of the open dialog component.
117#[relm4::component(pub)]
118impl<S: Select + 'static> SimpleComponent for OpenDialogInner<S> {
119    type Init = OpenDialogSettings;
120    type Input = OpenDialogMsg;
121    type Output = OpenDialogResponse<S>;
122
123    view! {
124        gtk::FileChooserNative {
125            set_action: if settings.folder_mode {
126                gtk::FileChooserAction::SelectFolder
127            } else {
128                gtk::FileChooserAction::Open
129            },
130
131            set_select_multiple: S::SELECT_MULTIPLE,
132            set_create_folders: settings.create_folders,
133            set_modal: settings.is_modal,
134            set_accept_label: Some(&settings.accept_label),
135            set_cancel_label: Some(&settings.cancel_label),
136            #[iterate]
137            add_filter: &settings.filters,
138
139            #[watch]
140            set_visible: model.visible,
141
142            connect_response[sender] => move |dialog, res_ty| {
143                match res_ty {
144                    gtk::ResponseType::Accept => {
145                        let selection = S::select(dialog);
146                        sender.output(OpenDialogResponse::Accept(selection)).unwrap();
147                    }
148                    _ => sender.output(OpenDialogResponse::Cancel).unwrap(),
149                }
150
151                sender.input(OpenDialogMsg::Hide);
152            }
153        }
154    }
155
156    fn init(
157        settings: Self::Init,
158        root: Self::Root,
159        sender: ComponentSender<Self>,
160    ) -> ComponentParts<Self> {
161        let model = OpenDialogInner {
162            visible: false,
163            _phantom: PhantomData,
164        };
165
166        let widgets = view_output!();
167
168        ComponentParts { model, widgets }
169    }
170
171    fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
172        match message {
173            OpenDialogMsg::Open => {
174                self.visible = true;
175            }
176            OpenDialogMsg::Hide => {
177                self.visible = false;
178            }
179        }
180    }
181}