// Map, wardley map editor for linux // Copyright (C) 2026 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . //! Dialog helper functions for file operations and error display. use std::path::PathBuf; use gtk::prelude::*; use relm4::gtk::gio; use relm4::{ComponentSender, adw, gtk}; use crate::AppModel; use crate::actions::{Action, ImageFormat}; use crate::constants; use crate::file_registry; use crate::tr; /// 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(&tr!("Wardley Map Files"))); filter } /// Shows an error dialog with the given title and message. pub fn show_error(window: &adw::Window, title: &str, message: &str) { let dialog = gtk::AlertDialog::builder() .modal(true) .buttons([&tr!("Cancel"), "Ok"]) .message(title) .detail(message) .build(); dialog.show(Some(window)); } /// Shows a file open dialog and handles the response. pub fn show_open_dialog( window: &adw::Window, is_empty_document: bool, sender: ComponentSender, ) { let dialog = gtk::FileDialog::builder() .title(tr!("Open Map")) .modal(true) .build(); let filter = create_file_filter(); let filters = gtk::gio::ListStore::new::(); filters.append(&filter); dialog.set_filters(Some(&filters)); let window_clone = window.clone(); gtk::glib::spawn_future_local(async move { if let Ok(file) = dialog.open_future(Some(&window_clone)).await && 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), None); } } }); } /// Shows a file save dialog and handles the response. pub fn show_save_dialog( window: &adw::Window, current_file: Option<&PathBuf>, close_after: bool, sender: ComponentSender, ) { let mut dialog_builder = gtk::FileDialog::builder() .title(tr!("Save Map As")) .modal(true); let filter = create_file_filter(); let filters = gtk::gio::ListStore::new::(); filters.append(&filter); dialog_builder = dialog_builder.filters(&filters); let default_filename = "map.wmap"; if let Some(current_path) = current_file { if let Some(parent) = current_path.parent() { let folder = gtk::gio::File::for_path(parent); dialog_builder = dialog_builder.initial_folder(&folder); } if let Some(filename) = current_path.file_name() { dialog_builder = dialog_builder.initial_name(filename.to_str().unwrap_or(default_filename)); } } else { dialog_builder = dialog_builder.initial_name(default_filename); } let dialog = dialog_builder.build(); let window_clone = window.clone(); gtk::glib::spawn_future_local(async move { if let Ok(file) = dialog.save_future(Some(&window_clone)).await && let Some(path) = file.path() { sender.input(Action::SaveToFile { path, close_after }); } }); } /// Shows a dialog asking whether to save unsaved changes before closing. pub fn show_unsaved_changes_dialog( window: &adw::Window, current_file: Option, sender: ComponentSender, ) { let dialog = gtk::AlertDialog::builder() .modal(true) .message(tr!("Unsaved Changes")) .detail(tr!("Save changes before closing?")) .buttons([tr!("Don't save"), tr!("Cancel"), tr!("Save")]) .cancel_button(1) .default_button(2) .build(); //dialog.show(Some(window)); let cancellable = gio::Cancellable::current(); let window_clone = window.clone(); dialog.choose( Some(window), cancellable.as_ref(), move |answer| match answer { Ok(0) => { if let Some(ref path) = current_file { file_registry::unregister_file(path); } window_clone.destroy(); } Ok(2) => { if current_file.is_some() { sender.input(Action::Save { close_after: true }); } else { sender.input(Action::SaveAs { close_after: true }); } } _ => {} }, ); } /// 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 => tr!("PNG Image"), ImageFormat::Svg => tr!("SVG Image"), }; filter.set_name(Some(&name)); filter } /// Shows an export image dialog and handles the response. pub fn show_export_dialog( window: &adw::Window, current_file: Option<&PathBuf>, sender: ComponentSender, ) { let png_filter = create_image_filter(ImageFormat::Png); let svg_filter = create_image_filter(ImageFormat::Svg); let filters = gtk::gio::ListStore::new::(); filters.append(&png_filter); filters.append(&svg_filter); let mut dialog_builder = gtk::FileDialog::builder() .title(tr!("Export Map as Image")) .modal(true) .filters(&filters) .default_filter(&png_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(current_path) = current_file && let Some(parent) = current_path.parent() { let folder = gtk::gio::File::for_path(parent); dialog_builder = dialog_builder.initial_folder(&folder); } dialog_builder = dialog_builder.initial_name(format!("{base_name}.png")); let dialog = dialog_builder.build(); let window_clone = window.clone(); gtk::glib::spawn_future_local(async move { if let Ok(file) = dialog.save_future(Some(&window_clone)).await && let Some(path) = file.path() { // Determine format from file extension let format = 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 }); } }); }