// 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::{ComponentSender, 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!("dialog.file_type.wmap"))); 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(&tr!("dialog.open.title")), Some(window), gtk::FileChooserAction::Open, &[ (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), (&tr!("dialog.action.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), None); } } }); dialog.present(); } /// Shows a file save dialog and handles the response. pub fn show_save_dialog( window: >k::Window, current_file: Option<&PathBuf>, close_after: bool, sender: ComponentSender, ) { let dialog = gtk::FileChooserDialog::new( Some(&tr!("dialog.save.title")), Some(window), gtk::FileChooserAction::Save, &[ (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), (&tr!("dialog.action.save"), gtk::ResponseType::Accept), ], ); dialog.add_filter(&create_file_filter()); 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.set_current_folder(Some(&folder)).ok(); } if let Some(filename) = current_path.file_name() { dialog.set_current_name(filename.to_str().unwrap_or(default_filename)); } } else { dialog.set_current_name(default_filename); } 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, tr!("dialog.unsaved_changes.message"), ); dialog.set_title(Some(&tr!("dialog.unsaved_changes.title"))); dialog.add_button(&tr!("dialog.action.dont_save"), gtk::ResponseType::Reject); dialog.add_button(&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel); dialog.add_button(&tr!("dialog.action.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 => tr!("dialog.file_type.png"), ImageFormat::Svg => tr!("dialog.file_type.svg"), }; 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<&PathBuf>, sender: ComponentSender, ) { let dialog = gtk::FileChooserDialog::new( Some(&tr!("dialog.export.title")), Some(window), gtk::FileChooserAction::Save, &[ (&tr!("dialog.action.cancel"), gtk::ResponseType::Cancel), (&tr!("dialog.action.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(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!("{base_name}.png")); // 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(); }