relm4_components/open_button/
mod.rs1use 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#[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)]
40pub struct OpenButtonSettings {
42 pub dialog_settings: OpenDialogSettings,
44 pub icon: Option<&'static str>,
46 pub text: &'static str,
48 pub recently_opened_files: Option<&'static str>,
51 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#[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 = >k::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}