diff options
| author | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-03-27 16:10:46 +0100 |
|---|---|---|
| committer | Rubén Beltrán del Río <jj@r.bdr.sh> | 2026-03-30 13:39:14 +0200 |
| commit | d01a4df546b553128400f6c68eefa8368b4113a8 (patch) | |
| tree | d290ca78b1215fb450ec7850a74b79fd0ae9707c /src/components/preferences_window.rs | |
| parent | 4dec344a7bcbf1d7d0bbbf985109df816470ecc6 (diff) | |
Use actual components for preference pages
Diffstat (limited to 'src/components/preferences_window.rs')
| -rw-r--r-- | src/components/preferences_window.rs | 455 |
1 files changed, 455 insertions, 0 deletions
diff --git a/src/components/preferences_window.rs b/src/components/preferences_window.rs new file mode 100644 index 0000000..ed18ba5 --- /dev/null +++ b/src/components/preferences_window.rs @@ -0,0 +1,455 @@ +// 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 <http://www.gnu.org/licenses/>. +use crate::icon_names::shipped::{AGENDA, BUILD, MAP, PAPYRUS, SETTINGS}; +use adw::prelude::*; +use relm4::prelude::*; +use std::convert::identity; + +use gtk::FileFilter; + +use crate::actions::{Action, PreferencesAction}; +use crate::components::preference_pages; +use crate::preferences::models::StageLabel; +use crate::preferences::models::{CustomStage, Template, UserPreferences}; +use crate::preferences::storage; +use crate::tr; + +pub struct PreferencesWindow { + preferences: UserPreferences, + window: adw::Window, + // Widget references for pages + general_page: Controller<preference_pages::General>, + editor_page: Controller<preference_pages::Editor>, + map_page: Controller<preference_pages::Map>, + stages_page: Controller<preference_pages::Stages>, + templates_page: Controller<preference_pages::Templates>, +} + +#[relm4::component(pub)] +impl SimpleComponent for PreferencesWindow { + type Init = UserPreferences; + type Input = PreferencesAction; + type Output = Action; + + 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.output(Action::PreferencesWindowClosed).ok(); + gtk::glib::Propagation::Proceed + }, + + adw::ToolbarView { + #[name = "header_bar"] + add_top_bar = &adw::HeaderBar { + #[wrap(Some)] + set_title_widget = &adw::ViewSwitcher { + set_stack: Some(&stack), + }, + }, + + #[name = "stack"] + adw::ViewStack { + set_vexpand: true, + + add_titled_with_icon[Some("general"), &tr!("preferences.menu.general"), SETTINGS] = model.general_page.widget(), + add_titled_with_icon[Some("editor"), &tr!("preferences.menu.editor"), BUILD] = model.editor_page.widget(), + add_titled_with_icon[Some("map"), &tr!("preferences.menu.map"), MAP] = model.map_page.widget(), + add_titled_with_icon[Some("stages"), &tr!("preferences.menu.stages"), AGENDA] = model.stages_page.widget(), + add_titled_with_icon[Some("templates"), &tr!("preferences.menu.templates"), PAPYRUS] = model.templates_page.widget(), + }, + + #[name = "switcher_bar"] + add_bottom_bar = &adw::ViewSwitcherBar { + set_stack: Some(&stack), + }, + }, + } + } + + fn init( + init: Self::Init, + root: Self::Root, + sender: ComponentSender<Self>, + ) -> ComponentParts<Self> { + let general_page = preference_pages::General::builder() + .launch(()) + .forward(sender.input_sender(), identity); + let editor_page = preference_pages::Editor::builder() + .launch(preference_pages::EditorInit { + font_size: init.editor_font_size, + soft_wrap: init.soft_wrap_lines, + use_custom_font: init.use_custom_editor_font, + custom_font: init.custom_editor_font_name.clone(), + }) + .forward(sender.input_sender(), identity); + let map_page = preference_pages::Map::builder() + .launch(preference_pages::MapInit { + show_background: init.show_map_background, + use_custom_font: init.use_custom_font, + use_smart_label_positioning: init.use_smart_label_positioning, + custom_font: init.custom_font_name.clone(), + default_export_format: init.default_export_format.clone(), + }) + .forward(sender.input_sender(), identity); + let stages_page = preference_pages::Stages::builder() + .launch(init.custom_stages.clone()) + .forward(sender.input_sender(), identity); + let templates_page = preference_pages::Templates::builder() + .launch(preference_pages::TemplatesInit { + templates: init.map_templates.clone(), + window: root.clone(), + }) + .forward(sender.input_sender(), identity); + + let model = PreferencesWindow { + preferences: init, + window: root.clone(), + general_page, + editor_page, + map_page, + stages_page, + templates_page, + }; + + let widgets = view_output!(); + + Self::init_breakpoints(&root, &widgets); + ComponentParts { model, widgets } + } + + fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { + match message { + PreferencesAction::Export => { + Self::show_export_dialog(sender, &self.window); + } + PreferencesAction::Import => { + Self::show_import_dialog(sender, &self.window); + } + PreferencesAction::ImportFromFile(path) => { + if storage::import_from_file(&mut self.preferences, &path).is_ok() { + self.sync_ui_from_preferences(); + self.save_and_notify(&sender); + } + } + PreferencesAction::ExportToFile(path) => { + let _ = storage::export_to_file(&self.preferences, &path); + } + PreferencesAction::ResetToDefaults => { + self.preferences = UserPreferences::default(); + self.sync_ui_from_preferences(); + self.save_and_notify(&sender); + } + PreferencesAction::SetShowMapBackground(show_background) => { + self.preferences.show_map_background = show_background; + self.map_page + .emit(preference_pages::MapAction::SetShowBackground( + show_background, + )); + self.save_and_notify(&sender); + } + PreferencesAction::SetUseCustomFont(use_custom_font) => { + self.preferences.use_custom_font = use_custom_font; + self.map_page + .emit(preference_pages::MapAction::SetUseCustomFont( + use_custom_font, + )); + self.save_and_notify(&sender); + } + PreferencesAction::SetCustomFontName(custom_font) => { + self.preferences.custom_font_name = custom_font.clone(); + self.map_page + .emit(preference_pages::MapAction::SetCustomFont(custom_font)); + self.save_and_notify(&sender); + } + PreferencesAction::SetDefaultExportFormat(default_export_format) => { + self.preferences.default_export_format = default_export_format.clone(); + self.map_page + .emit(preference_pages::MapAction::SetDefaultExportFormat( + default_export_format, + )); + self.save_and_notify(&sender); + } + PreferencesAction::SetUseSmartLabelPositioning(use_smart_label_positioning) => { + self.preferences.use_smart_label_positioning = use_smart_label_positioning; + self.map_page + .emit(preference_pages::MapAction::SetUseSmartLabelPositioning( + use_smart_label_positioning, + )); + self.save_and_notify(&sender); + } + PreferencesAction::SetUseCustomEditorFont(use_custom_font) => { + self.preferences.use_custom_editor_font = use_custom_font; + self.editor_page + .emit(preference_pages::EditorAction::SetUseCustomFont( + use_custom_font, + )); + self.save_and_notify(&sender); + } + PreferencesAction::SetCustomEditorFontName(font_name) => { + self.preferences.custom_editor_font_name = font_name.clone(); + self.editor_page + .emit(preference_pages::EditorAction::SetCustomFont(font_name)); + self.save_and_notify(&sender); + } + PreferencesAction::SetEditorFontSize(font_size) => { + self.preferences.editor_font_size = font_size; + self.editor_page + .emit(preference_pages::EditorAction::SetFontSize(font_size)); + self.save_and_notify(&sender); + } + PreferencesAction::SetSoftWrapLines(soft_wrap_lines) => { + self.preferences.soft_wrap_lines = soft_wrap_lines; + self.editor_page + .emit(preference_pages::EditorAction::SetSoftWrap(soft_wrap_lines)); + self.save_and_notify(&sender); + } + PreferencesAction::AddTemplate(name) => self.handle_add_template(name, &sender), + PreferencesAction::RemoveTemplate(id) => self.handle_remove_template(id, &sender), + PreferencesAction::SetTemplateContent(id, content) => { + self.handle_set_template_content(id, content, &sender); + } + PreferencesAction::SetTemplateName(id, name) => { + self.handle_set_template_name(id, name, &sender); + } + PreferencesAction::SetDefaultTemplate(id) => { + self.handle_set_default_template(id, &sender); + } + PreferencesAction::AddCustomStage => self.handle_add_custom_stage(&sender), + PreferencesAction::RemoveCustomStage(id) => { + self.handle_remove_custom_stage(id, &sender); + } + PreferencesAction::SetCustomStageName(id, name) => { + self.handle_set_custom_stage_name(id, name, &sender); + } + PreferencesAction::SetCustomStageLabel(id, label, value) => { + self.handle_set_custom_stage_label(id, label, value, &sender); + } + } + } +} + +impl PreferencesWindow { + fn save_and_notify(&self, sender: &ComponentSender<Self>) { + let _ = storage::save(&self.preferences); + sender + .output(Action::PreferencesChanged(self.preferences.clone())) + .ok(); + } + + // TODO: Figure out how to best do this with messages. + fn sync_ui_from_preferences(&mut self) {} + + fn handle_add_template(&mut self, name: String, sender: &ComponentSender<Self>) { + let template = Template::new(name, String::new(), false); + self.preferences.map_templates.push(template.clone()); + self.templates_page + .emit(preference_pages::TemplatesAction::TemplateAdded(template)); + self.save_and_notify(sender); + } + + fn handle_remove_template(&mut self, id: uuid::Uuid, sender: &ComponentSender<Self>) { + self.preferences.map_templates.retain(|t| t.id != id); + self.templates_page + .emit(preference_pages::TemplatesAction::TemplateRemoved(id)); + self.save_and_notify(sender); + } + + fn handle_set_template_content( + &mut self, + id: uuid::Uuid, + content: String, + sender: &ComponentSender<Self>, + ) { + if let Some(template) = self + .preferences + .map_templates + .iter_mut() + .find(|t| t.id == id) + { + template.content = content; + self.templates_page + .emit(preference_pages::TemplatesAction::TemplateUpdated( + template.clone(), + )); + self.save_and_notify(sender); + } + } + + fn handle_set_template_name( + &mut self, + id: uuid::Uuid, + name: String, + sender: &ComponentSender<Self>, + ) { + if let Some(template) = self + .preferences + .map_templates + .iter_mut() + .find(|t| t.id == id) + { + template.name = name; + self.templates_page + .emit(preference_pages::TemplatesAction::TemplateUpdated( + template.clone(), + )); + self.save_and_notify(sender); + } + } + + fn handle_set_default_template(&mut self, id: uuid::Uuid, sender: &ComponentSender<Self>) { + self.preferences.set_default_template(id); + self.templates_page + .emit(preference_pages::TemplatesAction::UpdateAllTemplates( + self.preferences.map_templates.clone(), + )); + self.save_and_notify(&sender); + } + + fn handle_add_custom_stage(&mut self, sender: &ComponentSender<Self>) { + let stage = CustomStage::placeholder_default(); + self.preferences.custom_stages.push(stage.clone()); + self.stages_page + .emit(preference_pages::StagesAction::CustomStageAdded(stage)); + self.save_and_notify(sender); + } + + fn handle_remove_custom_stage(&mut self, id: uuid::Uuid, sender: &ComponentSender<Self>) { + self.preferences.custom_stages.retain(|s| s.id != id); + self.stages_page + .emit(preference_pages::StagesAction::CustomStageRemoved(id)); + self.save_and_notify(sender); + } + + fn handle_set_custom_stage_name( + &mut self, + id: uuid::Uuid, + name: String, + sender: &ComponentSender<Self>, + ) { + if let Some(stage) = self + .preferences + .custom_stages + .iter_mut() + .find(|s| s.id == id) + { + stage.name = name; + self.stages_page + .emit(preference_pages::StagesAction::CustomStageUpdated( + stage.clone(), + )); + self.save_and_notify(sender); + } + } + + fn handle_set_custom_stage_label( + &mut self, + id: uuid::Uuid, + label: StageLabel, + value: String, + sender: &ComponentSender<Self>, + ) { + 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.stages_page + .emit(preference_pages::StagesAction::CustomStageUpdated( + stage.clone(), + )); + self.save_and_notify(sender); + } + } + + // Initializes the breakpoint logic + fn init_breakpoints(root: &adw::Window, widgets: &PreferencesWindowWidgets) { + let breakpoint = adw::Breakpoint::new(adw::BreakpointCondition::new_length( + adw::BreakpointConditionLengthType::MaxWidth, + 550.0, + adw::LengthUnit::Sp, + )); + breakpoint.add_setter(&widgets.switcher_bar, "reveal", Some(&true.to_value())); + breakpoint.add_setter( + &widgets.header_bar, + "title-widget", + Some(&None::<gtk::Widget>.to_value()), + ); + root.add_breakpoint(breakpoint); + } + fn show_export_dialog(sender: ComponentSender<Self>, window: &adw::Window) { + let filter = FileFilter::new(); + filter.add_pattern("*.json"); + filter.set_name(Some(&tr!("dialog.file_type.json"))); + + let filters = gtk::gio::ListStore::new::<gtk::FileFilter>(); + filters.append(&filter); + + let dialog = gtk::FileDialog::builder() + .title(tr!("dialog.export_preferences.title")) + .modal(true) + .filters(&filters) + .initial_name("preferences.json") + .build(); + + let sender = sender.clone(); + let window = window.clone(); + gtk::glib::spawn_future_local(async move { + if let Ok(file) = dialog.save_future(Some(&window)).await + && let Some(path) = file.path() + { + sender.input(PreferencesAction::ExportToFile(path)); + } + }); + } + + fn show_import_dialog(sender: ComponentSender<Self>, window: &adw::Window) { + let filter = FileFilter::new(); + filter.add_pattern("*.json"); + filter.set_name(Some(&tr!("dialog.file_type.json"))); + + let filters = gtk::gio::ListStore::new::<gtk::FileFilter>(); + filters.append(&filter); + + let dialog = gtk::FileDialog::builder() + .title(tr!("dialog.import_preferences.title")) + .modal(true) + .filters(&filters) + .build(); + + let sender = sender.clone(); + let window = window.clone(); + gtk::glib::spawn_future_local(async move { + if let Ok(file) = dialog.open_future(Some(&window)).await + && let Some(path) = file.path() + { + sender.input(PreferencesAction::ImportFromFile(path)); + } + }); + } +} |