From e9b80817f9c9b1d83b6f5cf0a916bdc39f0ee833 Mon Sep 17 00:00:00 2001 From: rootiest Date: Mon, 3 Aug 2026 23:03:24 -0400 Subject: [PATCH] feat: add unsaved changes confirmation prompts for window close and navigation using gtk::AlertDialog --- src/composite_icon.rs | 4 +- src/main.rs | 105 ++++++++++++++++++++++++++++++++++-------- src/ui/editor.rs | 39 ++++++++++++---- 3 files changed, 120 insertions(+), 28 deletions(-) diff --git a/src/composite_icon.rs b/src/composite_icon.rs index 7ef8d97..05d5a9d 100644 --- a/src/composite_icon.rs +++ b/src/composite_icon.rs @@ -6,6 +6,7 @@ pub struct CompositeIcon { pub emblem_x: f64, pub emblem_y: f64, pub emblem_size: f64, + pub is_modified: bool, } impl CompositeIcon { @@ -16,6 +17,7 @@ impl CompositeIcon { emblem_x: 32.0, emblem_y: 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 ); - let mut base_svg = strip_dimensions(&self.bg.content); + let base_svg = strip_dimensions(&self.bg.content); if let Some(idx) = base_svg.rfind("") { let mut new_svg = base_svg[..idx].to_string(); new_svg.push_str(&emblem_group); diff --git a/src/main.rs b/src/main.rs index 821f545..af4badb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,10 +46,43 @@ fn main() -> glib::ExitCode { let stack = Stack::new(); window.set_child(Some(&stack)); - build_picker(&stack, &window, breeze_path.clone()); + let current_composite: Rc>>>> = Rc::new(RefCell::new(None)); + + { + let current_composite = Rc::clone(¤t_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(¤t_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(¤t_composite)); if let Some(Ok(path)) = initial_icon { - build_editor(&stack, &window, path, breeze_path); + build_editor(&stack, &window, path, breeze_path, Rc::clone(¤t_composite)); } window.present(); @@ -81,19 +114,20 @@ fn resolve_icon_arg(arg: &str, breeze_path: &Path) -> anyhow::Result { 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>>>>) { let stack_for_callback = stack.clone(); let window_for_callback = window.clone(); let breeze_for_editor = breeze_path.clone(); + let comp_callback = Rc::clone(¤t_composite); 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.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>>>>) { let mut scheme = match ColorScheme::load(&icon_path) { Ok(s) => s, Err(e) => { @@ -104,6 +138,7 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b scheme.auto_save = false; 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); @@ -112,6 +147,7 @@ fn build_editor(stack: &Stack, window: &ApplicationWindow, icon_path: PathBuf, b let stack = stack.clone(); let window = window.clone(); let editor_container = editor_container.clone(); + let current_comp = Rc::clone(¤t_composite); Rc::new(move || { 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 window_clone = window.clone(); let comp = Rc::clone(&composite); + let comp_for_back = Rc::clone(&comp); + let current_comp_for_back = Rc::clone(¤t_comp); let page = ui::editor::build_editor_page( Rc::clone(&composite), 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(¤t_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 || { stack_emblem.set_visible_child_name("emblem_picker"); }, - move || { - let dialog = gtk::FileDialog::builder() - .title("Save Composite Icon As...") - .build(); - let c = Rc::clone(&comp); - dialog.save(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| { - 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()); + { + let window_clone = window.clone(); + let comp = Rc::clone(&comp); + move || { + let dialog = gtk::FileDialog::builder() + .title("Save Composite Icon As...") + .build(); + let c = Rc::clone(&comp); + dialog.save(Some(&window_clone), gtk::gio::Cancellable::NONE, move |res| { + 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); @@ -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| { if let Ok(mut emblem_scheme) = ColorScheme::load_with_namespace(&emblem_path, "Emblem") { 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(); stack_emblem_callback.set_visible_child_name("editor"); } else { diff --git a/src/ui/editor.rs b/src/ui/editor.rs index 1a31ea2..233c2b5 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -129,24 +129,36 @@ pub fn build_editor_page( let composite = Rc::clone(&composite); let refresh_preview = refresh_preview.clone(); scale_x.connect_value_changed(move |s| { - composite.borrow_mut().emblem_x = s.value(); - refresh_preview(); + let mut comp = composite.borrow_mut(); + 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 refresh_preview = refresh_preview.clone(); scale_y.connect_value_changed(move |s| { - composite.borrow_mut().emblem_y = s.value(); - refresh_preview(); + let mut comp = composite.borrow_mut(); + 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 refresh_preview = refresh_preview.clone(); scale_size.connect_value_changed(move |s| { - composite.borrow_mut().emblem_size = s.value(); - refresh_preview(); + let mut comp = composite.borrow_mut(); + 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 mut comp = composite.borrow_mut(); + let res; 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 { - 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 { refresh_preview(); @@ -230,6 +247,9 @@ pub fn build_editor_page( if let Some(emblem) = &mut comp.emblem { changed |= emblem.undo().unwrap_or(false); } + if changed { + comp.is_modified = true; + } changed }; if changed { @@ -249,6 +269,9 @@ pub fn build_editor_page( if let Some(emblem) = &mut comp.emblem { changed |= emblem.redo().unwrap_or(false); } + if changed { + comp.is_modified = true; + } changed }; if changed {