feat: add unsaved changes confirmation prompts for window close and navigation using gtk::AlertDialog

This commit is contained in:
2026-08-03 23:03:24 -04:00
parent 2db5ca2b2e
commit e9b80817f9
3 changed files with 120 additions and 28 deletions
+3 -1
View File
@@ -6,6 +6,7 @@ pub struct CompositeIcon {
pub emblem_x: f64, pub emblem_x: f64,
pub emblem_y: f64, pub emblem_y: f64,
pub emblem_size: f64, pub emblem_size: f64,
pub is_modified: bool,
} }
impl CompositeIcon { impl CompositeIcon {
@@ -16,6 +17,7 @@ impl CompositeIcon {
emblem_x: 32.0, emblem_x: 32.0,
emblem_y: 32.0, emblem_y: 32.0,
emblem_size: 32.0, emblem_size: 32.0,
is_modified: false,
} }
} }
@@ -104,7 +106,7 @@ impl CompositeIcon {
mapped_x, mapped_y, scale_x, scale_y, emblem_inner mapped_x, mapped_y, scale_x, scale_y, emblem_inner
); );
let mut base_svg = strip_dimensions(&self.bg.content); let base_svg = strip_dimensions(&self.bg.content);
if let Some(idx) = base_svg.rfind("</svg>") { if let Some(idx) = base_svg.rfind("</svg>") {
let mut new_svg = base_svg[..idx].to_string(); let mut new_svg = base_svg[..idx].to_string();
new_svg.push_str(&emblem_group); new_svg.push_str(&emblem_group);
+86 -19
View File
@@ -46,10 +46,43 @@ fn main() -> glib::ExitCode {
let stack = Stack::new(); let stack = Stack::new();
window.set_child(Some(&stack)); window.set_child(Some(&stack));
build_picker(&stack, &window, breeze_path.clone()); let current_composite: Rc<RefCell<Option<Rc<RefCell<CompositeIcon>>>>> = Rc::new(RefCell::new(None));
{
let current_composite = Rc::clone(&current_composite);
let window_clone = window.clone();
window.connect_close_request(move |_| {
if let Some(comp) = current_composite.borrow().as_ref() {
if comp.borrow().is_modified {
let dialog = gtk::AlertDialog::builder()
.message("You have unsaved changes. Are you sure you want to close?")
.detail("All unsaved changes will be lost.")
.buttons(["Cancel", "Discard"])
.cancel_button(0)
.default_button(1)
.build();
let window_clone2 = window_clone.clone();
let comp_ref = Rc::clone(&current_composite);
dialog.choose(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
if let Ok(1) = res {
if let Some(c) = comp_ref.borrow().as_ref() {
c.borrow_mut().is_modified = false;
}
window_clone2.close();
}
});
return glib::Propagation::Stop;
}
}
glib::Propagation::Proceed
});
}
build_picker(&stack, &window, breeze_path.clone(), Rc::clone(&current_composite));
if let Some(Ok(path)) = initial_icon { if let Some(Ok(path)) = initial_icon {
build_editor(&stack, &window, path, breeze_path); build_editor(&stack, &window, path, breeze_path, Rc::clone(&current_composite));
} }
window.present(); window.present();
@@ -81,19 +114,20 @@ fn resolve_icon_arg(arg: &str, breeze_path: &Path) -> anyhow::Result<PathBuf> {
Ok(under_breeze.canonicalize()?) Ok(under_breeze.canonicalize()?)
} }
fn build_picker(stack: &Stack, window: &ApplicationWindow, breeze_path: PathBuf) { fn build_picker(stack: &Stack, window: &ApplicationWindow, breeze_path: PathBuf, current_composite: Rc<RefCell<Option<Rc<RefCell<CompositeIcon>>>>>) {
let stack_for_callback = stack.clone(); let stack_for_callback = stack.clone();
let window_for_callback = window.clone(); let window_for_callback = window.clone();
let breeze_for_editor = breeze_path.clone(); let breeze_for_editor = breeze_path.clone();
let comp_callback = Rc::clone(&current_composite);
let widget = ui::picker::build_picker_page(breeze_path, move |icon_path| { let widget = ui::picker::build_picker_page(breeze_path, move |icon_path| {
build_editor(&stack_for_callback, &window_for_callback, icon_path, breeze_for_editor.clone()); build_editor(&stack_for_callback, &window_for_callback, icon_path, breeze_for_editor.clone(), Rc::clone(&comp_callback));
}); });
stack.add_named(&widget, Some("picker")); stack.add_named(&widget, Some("picker"));
stack.set_visible_child_name("picker"); stack.set_visible_child_name("picker");
} }
fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, breeze_path: PathBuf) { fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, breeze_path: PathBuf, current_composite: Rc<RefCell<Option<Rc<RefCell<CompositeIcon>>>>>) {
let mut scheme = match ColorScheme::load(&icon_path) { let mut scheme = match ColorScheme::load(&icon_path) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@@ -104,6 +138,7 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b
scheme.auto_save = false; scheme.auto_save = false;
let composite = Rc::new(RefCell::new(CompositeIcon::new(scheme))); let composite = Rc::new(RefCell::new(CompositeIcon::new(scheme)));
*current_composite.borrow_mut() = Some(Rc::clone(&composite));
let editor_container = gtk::Box::new(gtk::Orientation::Vertical, 0); let editor_container = gtk::Box::new(gtk::Orientation::Vertical, 0);
@@ -112,6 +147,7 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b
let stack = stack.clone(); let stack = stack.clone();
let window = window.clone(); let window = window.clone();
let editor_container = editor_container.clone(); let editor_container = editor_container.clone();
let current_comp = Rc::clone(&current_composite);
Rc::new(move || { Rc::new(move || {
while let Some(child) = editor_container.first_child() { while let Some(child) = editor_container.first_child() {
@@ -122,29 +158,57 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b
let stack_emblem = stack.clone(); let stack_emblem = stack.clone();
let window_clone = window.clone(); let window_clone = window.clone();
let comp = Rc::clone(&composite); let comp = Rc::clone(&composite);
let comp_for_back = Rc::clone(&comp);
let current_comp_for_back = Rc::clone(&current_comp);
let page = ui::editor::build_editor_page( let page = ui::editor::build_editor_page(
Rc::clone(&composite), Rc::clone(&composite),
move || { move || {
stack_clone.set_visible_child_name("picker"); if comp_for_back.borrow().is_modified {
let dialog = gtk::AlertDialog::builder()
.message("You have unsaved changes. Are you sure you want to discard them?")
.detail("All unsaved changes will be lost.")
.buttons(["Cancel", "Discard"])
.cancel_button(0)
.default_button(1)
.build();
let stack_clone = stack_clone.clone();
let comp_ref = Rc::clone(&current_comp_for_back);
dialog.choose(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
if let Ok(1) = res {
*comp_ref.borrow_mut() = None;
stack_clone.set_visible_child_name("picker");
}
});
} else {
*current_comp_for_back.borrow_mut() = None;
stack_clone.set_visible_child_name("picker");
}
}, },
move || { move || {
stack_emblem.set_visible_child_name("emblem_picker"); stack_emblem.set_visible_child_name("emblem_picker");
}, },
move || { {
let dialog = gtk::FileDialog::builder() let window_clone = window.clone();
.title("Save Composite Icon As...") let comp = Rc::clone(&comp);
.build(); move || {
let c = Rc::clone(&comp); let dialog = gtk::FileDialog::builder()
dialog.save(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| { .title("Save Composite Icon As...")
if let Ok(file) = res { .build();
if let Some(path) = file.path() { let c = Rc::clone(&comp);
if let Err(e) = std::fs::write(&path, c.borrow().render()) { dialog.save(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| {
eprintln!("Failed to save composite to {}: {e}", path.display()); if let Ok(file) = res {
if let Some(path) = file.path() {
if let Err(e) = std::fs::write(&path, c.borrow().render()) {
eprintln!("Failed to save composite to {}: {e}", path.display());
} else {
c.borrow_mut().is_modified = false;
}
} }
} }
} });
}); }
} }
); );
editor_container.append(&page); editor_container.append(&page);
@@ -165,7 +229,10 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b
let emblem_picker = ui::picker::build_picker_page(breeze_path, move |emblem_path| { let emblem_picker = ui::picker::build_picker_page(breeze_path, move |emblem_path| {
if let Ok(mut emblem_scheme) = ColorScheme::load_with_namespace(&emblem_path, "Emblem") { if let Ok(mut emblem_scheme) = ColorScheme::load_with_namespace(&emblem_path, "Emblem") {
emblem_scheme.auto_save = false; emblem_scheme.auto_save = false;
comp_for_picker.borrow_mut().set_emblem(emblem_scheme); let mut c = comp_for_picker.borrow_mut();
c.set_emblem(emblem_scheme);
c.is_modified = true;
drop(c);
rebuild_editor_clone(); rebuild_editor_clone();
stack_emblem_callback.set_visible_child_name("editor"); stack_emblem_callback.set_visible_child_name("editor");
} else { } else {
+31 -8
View File
@@ -129,24 +129,36 @@ pub fn build_editor_page(
let composite = Rc::clone(&composite); let composite = Rc::clone(&composite);
let refresh_preview = refresh_preview.clone(); let refresh_preview = refresh_preview.clone();
scale_x.connect_value_changed(move |s| { scale_x.connect_value_changed(move |s| {
composite.borrow_mut().emblem_x = s.value(); let mut comp = composite.borrow_mut();
refresh_preview(); if (comp.emblem_x - s.value()).abs() > f64::EPSILON {
comp.emblem_x = s.value();
comp.is_modified = true;
refresh_preview();
}
}); });
} }
{ {
let composite = Rc::clone(&composite); let composite = Rc::clone(&composite);
let refresh_preview = refresh_preview.clone(); let refresh_preview = refresh_preview.clone();
scale_y.connect_value_changed(move |s| { scale_y.connect_value_changed(move |s| {
composite.borrow_mut().emblem_y = s.value(); let mut comp = composite.borrow_mut();
refresh_preview(); if (comp.emblem_y - s.value()).abs() > f64::EPSILON {
comp.emblem_y = s.value();
comp.is_modified = true;
refresh_preview();
}
}); });
} }
{ {
let composite = Rc::clone(&composite); let composite = Rc::clone(&composite);
let refresh_preview = refresh_preview.clone(); let refresh_preview = refresh_preview.clone();
scale_size.connect_value_changed(move |s| { scale_size.connect_value_changed(move |s| {
composite.borrow_mut().emblem_size = s.value(); let mut comp = composite.borrow_mut();
refresh_preview(); if (comp.emblem_size - s.value()).abs() > f64::EPSILON {
comp.emblem_size = s.value();
comp.is_modified = true;
refresh_preview();
}
}); });
} }
@@ -197,11 +209,16 @@ pub fn build_editor_page(
); );
let changed = { let changed = {
let mut comp = composite.borrow_mut(); let mut comp = composite.borrow_mut();
let res;
if is_emblem { if is_emblem {
comp.emblem.as_mut().unwrap().update_color(&tag, &new_hex).unwrap_or(false) res = comp.emblem.as_mut().unwrap().update_color(&tag, &new_hex).unwrap_or(false);
} else { } else {
comp.bg.update_color(&tag, &new_hex).unwrap_or(false) res = comp.bg.update_color(&tag, &new_hex).unwrap_or(false);
} }
if res {
comp.is_modified = true;
}
res
}; };
if changed { if changed {
refresh_preview(); refresh_preview();
@@ -230,6 +247,9 @@ pub fn build_editor_page(
if let Some(emblem) = &mut comp.emblem { if let Some(emblem) = &mut comp.emblem {
changed |= emblem.undo().unwrap_or(false); changed |= emblem.undo().unwrap_or(false);
} }
if changed {
comp.is_modified = true;
}
changed changed
}; };
if changed { if changed {
@@ -249,6 +269,9 @@ pub fn build_editor_page(
if let Some(emblem) = &mut comp.emblem { if let Some(emblem) = &mut comp.emblem {
changed |= emblem.redo().unwrap_or(false); changed |= emblem.redo().unwrap_or(false);
} }
if changed {
comp.is_modified = true;
}
changed changed
}; };
if changed { if changed {