Skip to content

Commit 40a216e

Browse files
committed
Extract preferences dialog in its own widget
1 parent d4048c5 commit 40a216e

File tree

3 files changed

+389
-283
lines changed

3 files changed

+389
-283
lines changed

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod distrobox_task;
2929
mod exportable_apps_dialog;
3030
mod gtk_utils;
3131
mod known_distros;
32+
mod preferences_dialog;
3233
mod remote_resource;
3334
mod sidebar_row;
3435
mod store;

src/preferences_dialog.rs

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
use crate::root_store::RootStore;
2+
use crate::supported_terminals;
3+
use crate::terminal_combo_row::TerminalComboRow;
4+
use adw::prelude::*;
5+
use adw::subclass::prelude::*;
6+
use glib::{clone, derived_properties, Properties};
7+
use gtk::{glib, gio};
8+
use std::cell::RefCell;
9+
use tracing::error;
10+
11+
mod imp {
12+
use super::*;
13+
14+
#[derive(Properties)]
15+
#[properties(wrapper_type = super::PreferencesDialog)]
16+
pub struct PreferencesDialog {
17+
#[property(get, set, construct)]
18+
pub root_store: RefCell<RootStore>,
19+
20+
pub terminal_combo_row: RefCell<Option<TerminalComboRow>>,
21+
pub delete_btn: gtk::Button,
22+
pub add_terminal_btn: gtk::Button,
23+
}
24+
25+
impl Default for PreferencesDialog {
26+
fn default() -> Self {
27+
Self {
28+
root_store: RefCell::new(RootStore::default()),
29+
terminal_combo_row: RefCell::new(None),
30+
delete_btn: gtk::Button::new(),
31+
add_terminal_btn: gtk::Button::new(),
32+
}
33+
}
34+
}
35+
36+
#[derived_properties]
37+
impl ObjectImpl for PreferencesDialog {
38+
fn constructed(&self) {
39+
self.parent_constructed();
40+
let obj = self.obj();
41+
42+
obj.set_title("Preferences");
43+
44+
let page = adw::PreferencesPage::new();
45+
46+
// Terminal Settings Group
47+
let terminal_group = adw::PreferencesGroup::new();
48+
terminal_group.set_title("Terminal Settings");
49+
50+
// Initialize terminal combo row
51+
let terminal_combo_row = TerminalComboRow::new_with_params(obj.root_store());
52+
self.terminal_combo_row.replace(Some(terminal_combo_row.clone()));
53+
54+
// Initialize delete button
55+
self.delete_btn.set_label("Delete");
56+
self.delete_btn.add_css_class("destructive-action");
57+
self.delete_btn.add_css_class("pill");
58+
59+
// Set initial delete button state
60+
if let Some(selected) = terminal_combo_row.selected_item() {
61+
let selected_name = selected
62+
.downcast_ref::<gtk::StringObject>()
63+
.unwrap()
64+
.string();
65+
let is_read_only = obj
66+
.root_store()
67+
.terminal_repository()
68+
.is_read_only(&selected_name);
69+
70+
self.delete_btn.set_sensitive(!is_read_only);
71+
}
72+
73+
// Connect delete button
74+
self.delete_btn.connect_clicked(clone!(
75+
#[weak]
76+
obj,
77+
move |_| {
78+
obj.handle_delete_terminal();
79+
}
80+
));
81+
82+
// Update delete button when selection changes
83+
terminal_combo_row.connect_selected_item_notify(clone!(
84+
#[weak]
85+
obj,
86+
move |_| {
87+
obj.update_delete_button_state();
88+
}
89+
));
90+
91+
terminal_group.add(&terminal_combo_row);
92+
93+
// Initialize add terminal button
94+
self.add_terminal_btn.set_label("Add Custom");
95+
self.add_terminal_btn.add_css_class("pill");
96+
self.add_terminal_btn.set_halign(gtk::Align::Start);
97+
98+
// Connect add terminal button
99+
self.add_terminal_btn.connect_clicked(clone!(
100+
#[weak]
101+
obj,
102+
move |_| {
103+
obj.show_add_terminal_dialog();
104+
}
105+
));
106+
107+
let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 12);
108+
button_box.set_margin_start(12);
109+
button_box.set_margin_end(12);
110+
button_box.set_margin_top(12);
111+
button_box.set_margin_bottom(12);
112+
113+
button_box.append(&self.delete_btn);
114+
button_box.append(&self.add_terminal_btn);
115+
terminal_group.add(&button_box);
116+
117+
page.add(&terminal_group);
118+
obj.add(&page);
119+
}
120+
}
121+
122+
#[glib::object_subclass]
123+
impl ObjectSubclass for PreferencesDialog {
124+
const NAME: &'static str = "PreferencesDialog";
125+
type Type = super::PreferencesDialog;
126+
type ParentType = adw::PreferencesDialog;
127+
}
128+
129+
impl WidgetImpl for PreferencesDialog {}
130+
impl AdwDialogImpl for PreferencesDialog {}
131+
impl PreferencesDialogImpl for PreferencesDialog {}
132+
}
133+
134+
glib::wrapper! {
135+
pub struct PreferencesDialog(ObjectSubclass<imp::PreferencesDialog>)
136+
@extends adw::PreferencesDialog, adw::Dialog, gtk::Widget,
137+
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
138+
}
139+
140+
impl PreferencesDialog {
141+
pub fn new(root_store: RootStore) -> Self {
142+
glib::Object::builder()
143+
.property("root-store", root_store)
144+
.build()
145+
}
146+
147+
fn update_delete_button_state(&self) {
148+
let imp = self.imp();
149+
if let (Some(terminal_combo_row), Some(delete_btn)) = (
150+
imp.terminal_combo_row.borrow().as_ref(),
151+
Some(&imp.delete_btn)
152+
) {
153+
if let Some(selected) = terminal_combo_row.selected_item() {
154+
let selected_name = selected
155+
.downcast_ref::<gtk::StringObject>()
156+
.unwrap()
157+
.string();
158+
let is_read_only = self
159+
.root_store()
160+
.terminal_repository()
161+
.is_read_only(&selected_name);
162+
163+
delete_btn.set_sensitive(!is_read_only);
164+
}
165+
}
166+
}
167+
168+
fn handle_delete_terminal(&self) {
169+
let imp = self.imp();
170+
let terminal_combo_row = match imp.terminal_combo_row.borrow().as_ref() {
171+
Some(row) => row.clone(),
172+
None => return,
173+
};
174+
175+
let selected = terminal_combo_row
176+
.selected_item()
177+
.and_downcast_ref::<gtk::StringObject>()
178+
.unwrap()
179+
.string();
180+
181+
let dialog = adw::AlertDialog::builder()
182+
.heading("Delete this terminal?")
183+
.body(format!(
184+
"{} will be removed from the terminal list.\nThis action cannot be undone.",
185+
selected
186+
))
187+
.close_response("cancel")
188+
.default_response("cancel")
189+
.build();
190+
dialog.add_response("cancel", "Cancel");
191+
dialog.add_response("delete", "Delete");
192+
193+
dialog.set_response_appearance("delete", adw::ResponseAppearance::Destructive);
194+
dialog.connect_response(
195+
Some("delete"),
196+
clone!(
197+
#[weak(rename_to = this)]
198+
self,
199+
#[strong]
200+
selected,
201+
move |d, _| {
202+
match this
203+
.root_store()
204+
.terminal_repository()
205+
.delete_terminal(&selected)
206+
{
207+
Ok(_) => {
208+
glib::MainContext::ref_thread_default().spawn_local(
209+
async move {
210+
if let Some(terminal_combo_row) = this.imp().terminal_combo_row.borrow().as_ref() {
211+
terminal_combo_row.reload_terminals();
212+
terminal_combo_row.set_selected_by_name(
213+
&this.root_store()
214+
.terminal_repository()
215+
.default_terminal()
216+
.await
217+
.map(|x| x.name)
218+
.unwrap_or_default(),
219+
);
220+
}
221+
222+
this.add_toast(adw::Toast::new(
223+
"Terminal removed successfully",
224+
));
225+
},
226+
);
227+
}
228+
Err(err) => {
229+
error!(error = %err, "Failed to delete terminal");
230+
this.add_toast(adw::Toast::new("Failed to delete terminal"));
231+
}
232+
}
233+
d.close();
234+
}
235+
),
236+
);
237+
238+
dialog.present(Some(self));
239+
}
240+
241+
fn show_add_terminal_dialog(&self) {
242+
let custom_dialog = adw::Dialog::new();
243+
custom_dialog.set_title("Add Custom Terminal");
244+
245+
let toolbar_view = adw::ToolbarView::new();
246+
toolbar_view.add_top_bar(&adw::HeaderBar::new());
247+
248+
let content = gtk::Box::new(gtk::Orientation::Vertical, 12);
249+
content.set_margin_start(12);
250+
content.set_margin_end(12);
251+
content.set_margin_top(12);
252+
content.set_margin_bottom(12);
253+
254+
let group = adw::PreferencesGroup::new();
255+
256+
// Name entry
257+
let name_entry = adw::EntryRow::builder().title("Terminal Name").build();
258+
259+
// Program entry
260+
let program_entry = adw::EntryRow::builder().title("Program Path").build();
261+
262+
// Separator argument entry
263+
let separator_entry = adw::EntryRow::builder()
264+
.title("Separator Argument")
265+
.build();
266+
267+
group.add(&name_entry);
268+
group.add(&program_entry);
269+
group.add(&separator_entry);
270+
content.append(&group);
271+
272+
// Add note about separator
273+
let info_label = gtk::Label::new(Some(
274+
"The separator argument is used to pass commands to the terminal.\n\
275+
Examples: '--' for GNOME Terminal, '-e' for xterm",
276+
));
277+
info_label.add_css_class("caption");
278+
info_label.add_css_class("dim-label");
279+
info_label.set_wrap(true);
280+
info_label.set_xalign(0.0);
281+
info_label.set_margin_start(12);
282+
content.append(&info_label);
283+
284+
// Buttons
285+
let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
286+
button_box.set_margin_top(12);
287+
button_box.set_homogeneous(true);
288+
289+
let cancel_btn = gtk::Button::with_label("Cancel");
290+
cancel_btn.add_css_class("pill");
291+
292+
let save_btn = gtk::Button::with_label("Save");
293+
save_btn.add_css_class("suggested-action");
294+
save_btn.add_css_class("pill");
295+
296+
button_box.append(&cancel_btn);
297+
button_box.append(&save_btn);
298+
content.append(&button_box);
299+
300+
toolbar_view.set_content(Some(&content));
301+
custom_dialog.set_child(Some(&toolbar_view));
302+
303+
// Connect button handlers
304+
cancel_btn.connect_clicked(clone!(
305+
#[weak]
306+
custom_dialog,
307+
move |_| {
308+
custom_dialog.close();
309+
}
310+
));
311+
312+
save_btn.connect_clicked(clone!(
313+
#[weak]
314+
custom_dialog,
315+
#[weak]
316+
name_entry,
317+
#[weak]
318+
program_entry,
319+
#[weak]
320+
separator_entry,
321+
#[weak(rename_to = this)]
322+
self,
323+
move |_| {
324+
let name = name_entry.text().to_string();
325+
let program = program_entry.text().to_string();
326+
let separator_arg = separator_entry.text().to_string();
327+
328+
// Validate inputs
329+
if name.is_empty() || program.is_empty() || separator_arg.is_empty() {
330+
this.add_toast(adw::Toast::new("All fields are required"));
331+
return;
332+
}
333+
334+
// Create and save the terminal
335+
let terminal = supported_terminals::Terminal {
336+
name,
337+
program,
338+
separator_arg,
339+
read_only: false,
340+
};
341+
342+
match this
343+
.root_store()
344+
.terminal_repository()
345+
.save_terminal(terminal.clone())
346+
{
347+
Ok(_) => {
348+
// Show success toast
349+
let toast = adw::Toast::new("Custom terminal added successfully");
350+
351+
if let Some(terminal_combo_row) = this.imp().terminal_combo_row.borrow().as_ref() {
352+
terminal_combo_row.reload_terminals();
353+
terminal_combo_row.set_selected_by_name(&terminal.name);
354+
}
355+
356+
this.add_toast(toast);
357+
custom_dialog.close();
358+
}
359+
Err(err) => {
360+
error!(error = %err, "Failed to save terminal");
361+
this.add_toast(adw::Toast::new("Failed to save terminal"));
362+
}
363+
}
364+
}
365+
));
366+
367+
custom_dialog.present(Some(self));
368+
}
369+
370+
fn add_toast(&self, toast: adw::Toast) {
371+
// Try to find a parent window with a toast overlay
372+
let mut widget = self.parent();
373+
while let Some(parent) = widget {
374+
if let Some(window) = parent.downcast_ref::<crate::window::DistroShelfWindow>() {
375+
window.imp().toast_overlay.add_toast(toast);
376+
return;
377+
}
378+
widget = parent.parent();
379+
}
380+
381+
// Fallback: create a simple alert dialog if no toast overlay is found
382+
let alert = adw::AlertDialog::new(toast.title().as_deref(), None);
383+
alert.add_response("ok", "OK");
384+
alert.present(Some(self));
385+
}
386+
}

0 commit comments

Comments
 (0)