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