Skip to main content

relm4_components/open_button/
mod.rs

1//! Reusable and easily configurable open button dialog component.
2//!
3//! **[Example implementation](https://github.com/Relm4/Relm4/blob/main/relm4-components/examples/open_button.rs)**
4use relm4::factory::{DynamicIndex, FactoryComponent, FactoryVecDeque};
5use relm4::gtk::prelude::*;
6use relm4::{
7    Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent,
8    gtk,
9};
10
11use crate::open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings};
12
13use std::fs;
14use std::path::PathBuf;
15
16mod factory;
17
18use factory::FileListItem;
19
20/// Open button component.
21///
22/// Creates a button with custom text that can be used to open a file chooser dialog. If a file is
23/// chosen, then it will be emitted as an output. The component can also optionally display a
24/// popover list of open files if [`OpenButtonSettings::recently_opened_files`] is set to a value.
25#[tracker::track]
26#[derive(Debug)]
27pub struct OpenButton {
28    #[do_not_track]
29    config: OpenButtonSettings,
30    #[do_not_track]
31    dialog: Controller<OpenDialog>,
32    #[do_not_track]
33    recent_files: Option<FactoryVecDeque<FileListItem>>,
34    initialized: bool,
35    #[do_not_track]
36    reset_popover: bool,
37}
38
39#[derive(Debug)]
40/// Configuration for the open button component
41pub struct OpenButtonSettings {
42    /// Settings for the open file dialog.
43    pub dialog_settings: OpenDialogSettings,
44    /// Icon of the open button.
45    pub icon: Option<&'static str>,
46    /// Text of the open button.
47    pub text: &'static str,
48    /// Path to a file where recent files should be stored.
49    /// This list is updated fully automatically.
50    pub recently_opened_files: Option<&'static str>,
51    /// Maximum amount of recent files to store.
52    /// This is only used if a path for storing the recently opened files was set.
53    pub max_recent_files: usize,
54}
55
56#[doc(hidden)]
57#[derive(Debug)]
58pub enum OpenButtonMsg {
59    Open(PathBuf),
60    OpenRecent(DynamicIndex),
61    ShowDialog,
62    Ignore,
63}
64
65/// Widgets of the open button component
66#[relm4::component(pub)]
67impl SimpleComponent for OpenButton {
68    type Init = OpenButtonSettings;
69    type Input = OpenButtonMsg;
70    type Output = PathBuf;
71
72    view! {
73        gtk::Box {
74            add_css_class: relm4::css::LINKED,
75            gtk::Button {
76                connect_clicked => OpenButtonMsg::ShowDialog,
77
78                gtk::Box {
79                    set_orientation: gtk::Orientation::Horizontal,
80                    set_spacing: 5,
81
82                    gtk::Image {
83                        set_visible: model.config.icon.is_some(),
84                        set_icon_name: model.config.icon,
85                    },
86
87                    gtk::Label {
88                        set_label: model.config.text,
89                    }
90                }
91            },
92            gtk::MenuButton {
93                #[watch]
94                set_visible: model.config.recently_opened_files.is_some()
95                    && model.recent_files.is_some(),
96
97                #[watch]
98                set_sensitive: !model.recent_files
99                    .as_ref()
100                    .map(FactoryVecDeque::is_empty)
101                    .unwrap_or(false),
102
103                #[wrap(Some)]
104                #[name(popover)]
105                set_popover = &gtk::Popover {
106                    gtk::ScrolledWindow {
107                        set_hscrollbar_policy: gtk::PolicyType::Never,
108                        set_vscrollbar_policy: gtk::PolicyType::Automatic,
109                        set_propagate_natural_height: true,
110
111                        #[local_ref]
112                        recent_files_list -> gtk::ListBox {
113                            set_vexpand: true,
114                            set_hexpand: true,
115                            set_selection_mode: gtk::SelectionMode::None,
116                        }
117                    }
118                }
119            }
120        }
121    }
122
123    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
124        self.reset_popover = false;
125
126        match msg {
127            OpenButtonMsg::ShowDialog => {
128                self.dialog.emit(OpenDialogMsg::Open);
129            }
130            OpenButtonMsg::Open(path) => {
131                sender.output(path.clone()).unwrap();
132                self.reset_popover = true;
133
134                if let Some(recent_files) = &mut self.recent_files {
135                    let index = recent_files.iter().position(|item| item.path == path);
136
137                    if let Some(index) = index {
138                        recent_files.guard().remove(index);
139                    }
140
141                    if recent_files.len() < self.config.max_recent_files {
142                        recent_files.guard().push_front(path);
143                    }
144
145                    let contents = recent_files
146                        .iter()
147                        .filter_map(|recent_path| {
148                            recent_path.path.to_str().map(|s| format!("{s}\n"))
149                        })
150                        .collect::<String>();
151
152                    let _ = fs::write(self.config.recently_opened_files.unwrap(), contents);
153                }
154            }
155            OpenButtonMsg::OpenRecent(index) => {
156                if let Some(item) = self
157                    .recent_files
158                    .as_ref()
159                    .and_then(|recent_files| recent_files.get(index.current_index()))
160                {
161                    sender.input(OpenButtonMsg::Open(PathBuf::from(&item.path)));
162                }
163            }
164            OpenButtonMsg::Ignore => (),
165        }
166    }
167
168    fn pre_view() {
169        if self.reset_popover {
170            popover.popdown();
171        }
172    }
173
174    fn init(
175        settings: Self::Init,
176        root: Self::Root,
177        sender: ComponentSender<Self>,
178    ) -> ComponentParts<Self> {
179        let dialog = OpenDialog::builder()
180            .transient_for_native(&root)
181            .launch(settings.dialog_settings.clone())
182            .forward(sender.input_sender(), |response| match response {
183                OpenDialogResponse::Accept(path) => OpenButtonMsg::Open(path),
184                OpenDialogResponse::Cancel => OpenButtonMsg::Ignore,
185            });
186
187        let recent_files_list = <FileListItem as FactoryComponent>::ParentWidget::default();
188
189        let mut model = Self {
190            config: settings,
191            dialog,
192            initialized: false,
193            recent_files: None,
194            reset_popover: false,
195            tracker: 0,
196        };
197
198        if let Some(filename) = model.config.recently_opened_files {
199            let mut factory = FactoryVecDeque::builder()
200                .launch(recent_files_list.clone())
201                .forward(sender.input_sender(), |msg| msg);
202
203            if let Ok(entries) = fs::read_to_string(filename) {
204                let mut guard = factory.guard();
205                for entry in entries.lines() {
206                    guard.push_back(PathBuf::from(entry));
207                }
208            }
209
210            model.recent_files = Some(factory);
211        }
212
213        let widgets = view_output!();
214
215        ComponentParts { model, widgets }
216    }
217}