// 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 });
}
});
}