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, 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                #[wrap(Some)]
98                #[name(popover)]
99                set_popover = &gtk::Popover {
100                    gtk::ScrolledWindow {
101                        set_hscrollbar_policy: gtk::PolicyType::Never,
102                        set_min_content_width: 100,
103                        set_min_content_height: 100,
104                        set_min_content_height: 300,
105
106                        #[local_ref]
107                        recent_files_list -> gtk::Box {
108                            set_orientation: gtk::Orientation::Vertical,
109                            set_vexpand: true,
110                            set_hexpand: true,
111                        }
112                    }
113                }
114            }
115        }
116    }
117
118    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
119        self.reset_popover = false;
120
121        match msg {
122            OpenButtonMsg::ShowDialog => {
123                self.dialog.emit(OpenDialogMsg::Open);
124            }
125            OpenButtonMsg::Open(path) => {
126                sender.output(path.clone()).unwrap();
127                self.reset_popover = true;
128
129                if let Some(recent_files) = &mut self.recent_files {
130                    let index = recent_files.iter().position(|item| item.path == path);
131
132                    if let Some(index) = index {
133                        recent_files.guard().remove(index);
134                    }
135
136                    if recent_files.len() < self.config.max_recent_files {
137                        recent_files.guard().push_front(path);
138                    }
139
140                    let contents = recent_files
141                        .iter()
142                        .filter_map(|recent_path| {
143                            recent_path.path.to_str().map(|s| format!("{s}\n"))
144                        })
145                        .collect::<String>();
146
147                    let _ = fs::write(self.config.recently_opened_files.unwrap(), contents);
148                }
149            }
150            OpenButtonMsg::OpenRecent(index) => {
151                if let Some(item) = self
152                    .recent_files
153                    .as_ref()
154                    .and_then(|recent_files| recent_files.get(index.current_index()))
155                {
156                    sender.input(OpenButtonMsg::Open(PathBuf::from(&item.path)));
157                }
158            }
159            OpenButtonMsg::Ignore => (),
160        }
161    }
162
163    fn pre_view() {
164        if self.reset_popover {
165            popover.popdown();
166        }
167    }
168
169    fn init(
170        settings: Self::Init,
171        root: Self::Root,
172        sender: ComponentSender<Self>,
173    ) -> ComponentParts<Self> {
174        let dialog = OpenDialog::builder()
175            .transient_for_native(&root)
176            .launch(settings.dialog_settings.clone())
177            .forward(sender.input_sender(), |response| match response {
178                OpenDialogResponse::Accept(path) => OpenButtonMsg::Open(path),
179                OpenDialogResponse::Cancel => OpenButtonMsg::Ignore,
180            });
181
182        let recent_files_list = gtk::Box::default();
183
184        let mut model = Self {
185            config: settings,
186            dialog,
187            initialized: false,
188            recent_files: None,
189            reset_popover: false,
190            tracker: 0,
191        };
192
193        if let Some(filename) = model.config.recently_opened_files {
194            let mut factory = FactoryVecDeque::builder()
195                .launch(recent_files_list.clone())
196                .forward(sender.input_sender(), |msg| msg);
197
198            if let Ok(entries) = fs::read_to_string(filename) {
199                let mut guard = factory.guard();
200                for entry in entries.lines() {
201                    guard.push_back(PathBuf::from(entry));
202                }
203            }
204
205            model.recent_files = Some(factory);
206        }
207
208        let widgets = view_output!();
209
210        ComponentParts { model, widgets }
211    }
212}