// 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 .
// Suppress warnings from relm4 view! macro internals
#![allow(unused_assignments)]
use std::path::PathBuf;
use gtk::prelude::*;
use relm4::{ComponentParts, ComponentSender, SimpleComponent, adw, gtk};
use crate::tr;
use super::models::UserPreferences;
use super::pages::{editor, general, map, stages, templates};
use super::storage;
/// Messages for preferences window
#[derive(Debug)]
pub enum PreferencesInput {
// General page actions
Export,
Import,
ImportFromFile(PathBuf),
ExportToFile(PathBuf),
ResetToDefaults,
// Preference updates from pages
SetShowMapBackground(bool),
SetUseCustomFont(bool),
SetCustomFontName(String),
SetDefaultExportFormat(String),
SetUseSmartLabelPositioning(bool),
SetUseCustomEditorFont(bool),
SetCustomEditorFontName(String),
SetEditorFontSize(f64),
SetSoftWrapLines(bool),
// Templates
AddTemplate(String),
RemoveTemplate(uuid::Uuid),
SetTemplateContent(uuid::Uuid, String),
SetTemplateName(uuid::Uuid, String),
SetDefaultTemplate(uuid::Uuid),
// Custom stages
AddCustomStage,
RemoveCustomStage(uuid::Uuid),
SetCustomStageName(uuid::Uuid, String),
SetCustomStageLabel(uuid::Uuid, StageLabel, String),
// Close
Close,
}
/// Which stage label to update (Roman numerals I-IV)
#[derive(Debug, Clone, Copy)]
pub enum StageLabel {
I,
Ii,
Iii,
Iv,
}
/// Output messages to parent app
#[derive(Debug)]
pub enum PreferencesOutput {
PreferencesChanged(UserPreferences),
Closed,
}
pub struct PreferencesWindow {
preferences: UserPreferences,
window: adw::Window,
// Widget references for pages
general_page: general::GeneralPage,
editor_page: editor::EditorPage,
map_page: map::MapPage,
stages_page: stages::StagesPage,
templates_page: templates::TemplatesPage,
}
#[relm4::component(pub)]
impl SimpleComponent for PreferencesWindow {
type Init = UserPreferences;
type Input = PreferencesInput;
type Output = PreferencesOutput;
view! {
adw::Window {
set_title: Some(&tr!("preferences.title")),
set_default_width: 700,
set_default_height: 500,
set_modal: true,
set_resizable: true,
connect_close_request[sender] => move |_| {
sender.input(PreferencesInput::Close);
gtk::glib::Propagation::Proceed
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
gtk::HeaderBar {
#[wrap(Some)]
set_title_widget = >k::StackSwitcher {
set_stack: Some(&stack),
},
},
#[name = "stack"]
gtk::Stack {
set_transition_type: gtk::StackTransitionType::SlideLeftRight,
set_vexpand: true,
add_titled[Some("general"), &tr!("preferences.menu.general")] = &model.general_page.widget() -> gtk::Box {},
add_titled[Some("editor"), &tr!("preferences.menu.editor")] = &model.editor_page.widget() -> gtk::Box {},
add_titled[Some("map"), &tr!("preferences.menu.map")] = &model.map_page.widget() -> gtk::Box {},
add_titled[Some("stages"), &tr!("preferences.menu.stages")] = &model.stages_page.widget() -> gtk::Box {},
add_titled[Some("templates"), &tr!("preferences.menu.templates")] = &model.templates_page.widget() -> gtk::Box {},
},
},
}
}
fn init(
preferences: Self::Init,
root: Self::Root,
sender: ComponentSender,
) -> ComponentParts {
let general_page = general::GeneralPage::new(&sender.input_sender().clone());
let editor_page = editor::EditorPage::new(&preferences, &sender.input_sender().clone());
let map_page = map::MapPage::new(&preferences, &sender.input_sender().clone());
let stages_page = stages::StagesPage::new(&preferences, sender.input_sender().clone());
let templates_page =
templates::TemplatesPage::new(&preferences, sender.input_sender().clone());
let model = PreferencesWindow {
preferences,
window: root.clone(),
general_page,
editor_page,
map_page,
stages_page,
templates_page,
};
let widgets = view_output!();
ComponentParts { model, widgets }
}
fn update(&mut self, message: Self::Input, sender: ComponentSender) {
match message {
PreferencesInput::Export => {
general::show_export_dialog(sender.input_sender(), &self.window);
}
PreferencesInput::Import => {
general::show_import_dialog(sender.input_sender(), &self.window);
}
PreferencesInput::ImportFromFile(path) => {
if storage::import_from_file(&mut self.preferences, &path).is_ok() {
self.sync_ui_from_preferences();
self.save_and_notify(&sender);
}
}
PreferencesInput::ExportToFile(path) => {
let _ = storage::export_to_file(&self.preferences, &path);
}
PreferencesInput::ResetToDefaults => {
self.preferences = UserPreferences::default();
self.sync_ui_from_preferences();
self.save_and_notify(&sender);
}
PreferencesInput::SetShowMapBackground(v) => {
self.preferences.show_map_background = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetUseCustomFont(v) => {
self.preferences.use_custom_font = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetCustomFontName(v) => {
self.preferences.custom_font_name = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetDefaultExportFormat(v) => {
self.preferences.default_export_format = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetUseSmartLabelPositioning(v) => {
self.preferences.use_smart_label_positioning = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetUseCustomEditorFont(v) => {
self.preferences.use_custom_editor_font = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetCustomEditorFontName(v) => {
self.preferences.custom_editor_font_name = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetEditorFontSize(v) => {
self.preferences.editor_font_size = v;
self.save_and_notify(&sender);
}
PreferencesInput::SetSoftWrapLines(v) => {
self.preferences.soft_wrap_lines = v;
self.save_and_notify(&sender);
}
PreferencesInput::AddTemplate(name) => self.handle_add_template(name, &sender),
PreferencesInput::RemoveTemplate(id) => self.handle_remove_template(id, &sender),
PreferencesInput::SetTemplateContent(id, content) => {
self.handle_set_template_content(id, content, &sender);
}
PreferencesInput::SetTemplateName(id, name) => {
self.handle_set_template_name(id, name, &sender);
}
PreferencesInput::SetDefaultTemplate(id) => {
self.preferences.set_default_template(id);
self.templates_page.refresh(&self.preferences);
self.save_and_notify(&sender);
}
PreferencesInput::AddCustomStage => self.handle_add_custom_stage(&sender),
PreferencesInput::RemoveCustomStage(id) => {
self.handle_remove_custom_stage(id, &sender);
}
PreferencesInput::SetCustomStageName(id, name) => {
self.handle_set_custom_stage_name(id, name, &sender);
}
PreferencesInput::SetCustomStageLabel(id, label, value) => {
self.handle_set_custom_stage_label(id, label, value, &sender);
}
PreferencesInput::Close => {
sender.output(PreferencesOutput::Closed).ok();
}
}
}
}
impl PreferencesWindow {
fn save_and_notify(&self, sender: &ComponentSender) {
let _ = storage::save(&self.preferences);
sender
.output(PreferencesOutput::PreferencesChanged(
self.preferences.clone(),
))
.ok();
}
fn sync_ui_from_preferences(&mut self) {
self.editor_page.sync_from(&self.preferences);
self.map_page.sync_from(&self.preferences);
self.stages_page.refresh(&self.preferences);
self.templates_page.refresh(&self.preferences);
}
fn handle_add_template(&mut self, name: String, sender: &ComponentSender) {
let template = super::models::Template::new(name, String::new(), false);
self.preferences.map_templates.push(template);
self.templates_page.refresh(&self.preferences);
self.save_and_notify(sender);
}
fn handle_remove_template(&mut self, id: uuid::Uuid, sender: &ComponentSender) {
self.preferences.map_templates.retain(|t| t.id != id);
self.templates_page.refresh(&self.preferences);
self.save_and_notify(sender);
}
fn handle_set_template_content(
&mut self,
id: uuid::Uuid,
content: String,
sender: &ComponentSender,
) {
if let Some(template) = self
.preferences
.map_templates
.iter_mut()
.find(|t| t.id == id)
{
template.content = content;
self.save_and_notify(sender);
}
}
fn handle_set_template_name(
&mut self,
id: uuid::Uuid,
name: String,
sender: &ComponentSender,
) {
if let Some(template) = self
.preferences
.map_templates
.iter_mut()
.find(|t| t.id == id)
{
template.name = name;
self.templates_page.refresh(&self.preferences);
self.save_and_notify(sender);
}
}
fn handle_add_custom_stage(&mut self, sender: &ComponentSender) {
let stage = super::models::CustomStage::placeholder_default();
self.preferences.custom_stages.push(stage);
self.stages_page.refresh(&self.preferences);
self.save_and_notify(sender);
}
fn handle_remove_custom_stage(&mut self, id: uuid::Uuid, sender: &ComponentSender) {
self.preferences.custom_stages.retain(|s| s.id != id);
self.stages_page.refresh(&self.preferences);
self.save_and_notify(sender);
}
fn handle_set_custom_stage_name(
&mut self,
id: uuid::Uuid,
name: String,
sender: &ComponentSender,
) {
if let Some(stage) = self
.preferences
.custom_stages
.iter_mut()
.find(|s| s.id == id)
{
stage.name = name;
self.save_and_notify(sender);
}
}
fn handle_set_custom_stage_label(
&mut self,
id: uuid::Uuid,
label: StageLabel,
value: String,
sender: &ComponentSender,
) {
if let Some(stage) = self
.preferences
.custom_stages
.iter_mut()
.find(|s| s.id == id)
{
match label {
StageLabel::I => stage.stage.i = value,
StageLabel::Ii => stage.stage.ii = value,
StageLabel::Iii => stage.stage.iii = value,
StageLabel::Iv => stage.stage.iv = value,
}
self.save_and_notify(sender);
}
}
}