//! Dialog helper functions for file operations and error display. use std::path::PathBuf; use gtk::prelude::*; use relm4::{ComponentSender, gtk}; use crate::AppModel; use crate::actions::{Action, ImageFormat}; use crate::constants; use crate::file_registry; /// Creates a file filter for .wmap files in file pickers. pub fn create_file_filter() -> gtk::FileFilter { let filter = gtk::FileFilter::new(); filter.add_pattern(&format!("*.{}", constants::FILE_EXTENSION)); filter.add_mime_type(constants::MIME_TYPE); filter.set_name(Some("Wardley Map Files")); filter } /// Shows an error dialog with the given title and message. pub fn show_error(window: >k::Window, title: &str, message: &str) { let dialog = gtk::MessageDialog::new( Some(window), gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT, gtk::MessageType::Error, gtk::ButtonsType::Ok, message, ); dialog.set_title(Some(title)); dialog.connect_response(|dialog, _| { dialog.close(); }); dialog.show(); } /// Shows a file open dialog and handles the response. pub fn show_open_dialog( window: >k::Window, is_empty_document: bool, sender: ComponentSender, ) { let dialog = gtk::FileChooserDialog::new( Some("Open Map"), Some(window), gtk::FileChooserAction::Open, &[ ("Cancel", gtk::ResponseType::Cancel), ("Open", gtk::ResponseType::Accept), ], ); dialog.add_filter(&create_file_filter()); dialog.connect_response(move |dialog, response| { dialog.close(); if response == gtk::ResponseType::Accept && let Some(file) = dialog.file() && let Some(path) = file.path() { // Check if file is already open - if so, focus that window if let Some(existing_window) = file_registry::find_window_for_file(&path) { existing_window.present(); return; } if is_empty_document { sender.input(Action::LoadFile(path)); } else { AppModel::open_new_window(Some(path)); } } }); dialog.present(); } /// Shows a file save dialog and handles the response. pub fn show_save_dialog( window: >k::Window, current_file: Option, close_after: bool, sender: ComponentSender, ) { let dialog = gtk::FileChooserDialog::new( Some("Save Map As"), Some(window), gtk::FileChooserAction::Save, &[ ("Cancel", gtk::ResponseType::Cancel), ("Save", gtk::ResponseType::Accept), ], ); dialog.add_filter(&create_file_filter()); if let Some(ref current_path) = current_file { if let Some(parent) = current_path.parent() { let folder = gtk::gio::File::for_path(parent); dialog.set_current_folder(Some(&folder)).ok(); } if let Some(filename) = current_path.file_name() { dialog.set_current_name(filename.to_str().unwrap_or("map.wmap")); } } else { dialog.set_current_name("map.wmap"); } dialog.connect_response(move |dialog, response| { dialog.close(); if response == gtk::ResponseType::Accept && let Some(file) = dialog.file() && let Some(path) = file.path() { sender.input(Action::SaveToFile { path, close_after }); } }); dialog.present(); } /// Shows a dialog asking whether to save unsaved changes before closing. pub fn show_unsaved_changes_dialog( window: >k::Window, current_file: Option, sender: ComponentSender, ) { let dialog = gtk::MessageDialog::new( Some(window), gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT, gtk::MessageType::Warning, gtk::ButtonsType::None, "Save changes before closing?", ); dialog.set_title(Some("Unsaved Changes")); dialog.add_button("Don't Save", gtk::ResponseType::Reject); dialog.add_button("Cancel", gtk::ResponseType::Cancel); dialog.add_button("Save", gtk::ResponseType::Accept); let window = window.clone(); dialog.connect_response(move |dialog, response| { dialog.close(); match response { gtk::ResponseType::Accept => { if current_file.is_some() { sender.input(Action::Save { close_after: true }); } else { sender.input(Action::SaveAs { close_after: true }); } } gtk::ResponseType::Reject => { if let Some(ref path) = current_file { file_registry::unregister_file(path); } window.destroy(); } _ => {} } }); dialog.show(); } /// Creates a file filter for an image format. fn create_image_filter(format: ImageFormat) -> gtk::FileFilter { let filter = gtk::FileFilter::new(); filter.add_pattern(&format!("*.{}", format.extension())); filter.add_mime_type(format.mime_type()); let name = match format { ImageFormat::Png => "PNG Image", ImageFormat::Svg => "SVG Image", }; filter.set_name(Some(name)); filter } /// Shows an export image dialog and handles the response. pub fn show_export_dialog( window: >k::Window, current_file: Option, sender: ComponentSender, ) { let dialog = gtk::FileChooserDialog::new( Some("Export Map as Image"), Some(window), gtk::FileChooserAction::Save, &[ ("Cancel", gtk::ResponseType::Cancel), ("Export", gtk::ResponseType::Accept), ], ); let png_filter = create_image_filter(ImageFormat::Png); let svg_filter = create_image_filter(ImageFormat::Svg); dialog.add_filter(&png_filter); dialog.add_filter(&svg_filter); // Set default filename based on current file let base_name = current_file .as_ref() .and_then(|p| p.file_stem()) .and_then(|s| s.to_str()) .unwrap_or("map") .to_string(); if let Some(ref current_path) = current_file && let Some(parent) = current_path.parent() { let folder = gtk::gio::File::for_path(parent); dialog.set_current_folder(Some(&folder)).ok(); } dialog.set_current_name(&format!("{}.png", base_name)); // Update extension when filter changes { let png_filter_ref = png_filter.clone(); let base_name_clone = base_name.clone(); dialog.connect_notify_local(Some("filter"), move |dialog, _| { let ext = if dialog.filter().as_ref() == Some(&png_filter_ref) { "png" } else { "svg" }; dialog.set_current_name(&format!("{}.{}", base_name_clone, ext)); }); } let png_filter_clone = png_filter.clone(); dialog.connect_response(move |dialog, response| { dialog.close(); if response == gtk::ResponseType::Accept && let Some(file) = dialog.file() && let Some(path) = file.path() { // Determine format from selected filter or file extension let format = if dialog.filter().as_ref() == Some(&png_filter_clone) { ImageFormat::Png } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { match ext.to_lowercase().as_str() { "svg" => ImageFormat::Svg, _ => ImageFormat::Png, } } else { ImageFormat::Png }; // Ensure correct extension let path = if path.extension().is_none() { path.with_extension(format.extension()) } else { path }; sender.input(Action::ExportToFile { path, format }); } }); dialog.present(); }