relm4_components/open_button/
mod.rs1use 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#[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 #[wrap(Some)]
98 #[name(popover)]
99 set_popover = >k::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}