From d01a4df546b553128400f6c68eefa8368b4113a8 Mon Sep 17 00:00:00 2001 From: Rubén Beltrán del Río Date: Fri, 27 Mar 2026 16:10:46 +0100 Subject: Use actual components for preference pages --- src/preferences/pages/editor.rs | 222 ----------------------- src/preferences/pages/general.rs | 144 --------------- src/preferences/pages/map.rs | 212 ---------------------- src/preferences/pages/mod.rs | 21 --- src/preferences/pages/stages.rs | 253 --------------------------- src/preferences/pages/templates.rs | 349 ------------------------------------- 6 files changed, 1201 deletions(-) delete mode 100644 src/preferences/pages/editor.rs delete mode 100644 src/preferences/pages/general.rs delete mode 100644 src/preferences/pages/map.rs delete mode 100644 src/preferences/pages/mod.rs delete mode 100644 src/preferences/pages/stages.rs delete mode 100644 src/preferences/pages/templates.rs (limited to 'src/preferences/pages') diff --git a/src/preferences/pages/editor.rs b/src/preferences/pages/editor.rs deleted file mode 100644 index c0e9b41..0000000 --- a/src/preferences/pages/editor.rs +++ /dev/null @@ -1,222 +0,0 @@ -// 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 . - -use gtk::prelude::*; -use relm4::Sender; -use relm4::gtk; - -use crate::tr; -use crate::ui_helpers::create_switch_row; - -use super::super::models::UserPreferences; -use super::super::window::PreferencesInput; - -const DEFAULT_FONT_SIZE: f64 = 14.0; - -pub struct EditorPage { - container: gtk::Box, - font_size_scale: gtk::Scale, - soft_wrap_switch: gtk::Switch, - custom_font_switch: gtk::Switch, - font_button: gtk::FontDialogButton, -} - -impl EditorPage { - pub fn new(preferences: &UserPreferences, sender: &Sender) -> Self { - let container = gtk::Box::new(gtk::Orientation::Vertical, 20); - container.set_margin_top(20); - container.set_margin_bottom(20); - container.set_margin_start(20); - container.set_margin_end(20); - container.set_valign(gtk::Align::Start); - - let (font_size_box, font_size_scale) = Self::create_font_size_section(preferences); - container.append(&font_size_box); - - let (soft_wrap_switch, custom_font_switch) = - Self::create_style_section(&container, preferences); - - let font_button = Self::create_font_button(preferences); - container.append(&Self::create_font_row(&font_button)); - - Self::connect_signals( - sender, - &font_size_scale, - &soft_wrap_switch, - &custom_font_switch, - &font_button, - ); - - Self { - container, - font_size_scale, - soft_wrap_switch, - custom_font_switch, - font_button, - } - } - - fn create_font_size_section(preferences: &UserPreferences) -> (gtk::Box, gtk::Scale) { - let label = gtk::Label::new(Some(&tr!("preferences.editor.font_size.title"))); - label.add_css_class("heading"); - label.set_halign(gtk::Align::Start); - - let scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 7.0, 21.0, 1.0); - scale.set_value(preferences.editor_font_size); - scale.set_draw_value(true); - scale.set_hexpand(true); - #[allow(clippy::cast_possible_truncation)] - scale.set_format_value_func(|_, value| { - #[allow(clippy::float_cmp)] - if value == DEFAULT_FONT_SIZE { - format!( - "{} ({})", - value as i32, - tr!("preferences.editor.default_font_size") - ) - } else { - format!("{}", value as i32) - } - }); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 10); - vbox.append(&label); - vbox.append(&scale); - - (vbox, scale) - } - - fn create_style_section( - container: >k::Box, - preferences: &UserPreferences, - ) -> (gtk::Switch, gtk::Switch) { - let label = gtk::Label::new(Some(&tr!("preferences.editor.editor_style.title"))); - label.add_css_class("heading"); - label.set_halign(gtk::Align::Start); - label.set_margin_top(10); - container.append(&label); - - let soft_wrap_row = create_switch_row( - &tr!("preferences.editor.editor_style.soft_wrap_lines"), - preferences.soft_wrap_lines, - ); - container.append(&soft_wrap_row.0); - - let custom_font_row = create_switch_row( - &tr!("preferences.editor.editor_style.use_custom_font"), - preferences.use_custom_editor_font, - ); - container.append(&custom_font_row.0); - - (soft_wrap_row.1, custom_font_row.1) - } - - fn create_font_button(preferences: &UserPreferences) -> gtk::FontDialogButton { - let font_dialog = gtk::FontDialog::new(); - let font_button = gtk::FontDialogButton::new(Some(font_dialog)); - #[allow(clippy::cast_possible_truncation)] - let font_desc = gtk::pango::FontDescription::from_string(&format!( - "{} {}", - preferences.custom_editor_font_name, preferences.editor_font_size as i32 - )); - font_button.set_font_desc(&font_desc); - font_button.set_sensitive(preferences.use_custom_editor_font); - font_button.set_hexpand(true); - font_button.set_use_font(true); - font_button.set_use_size(false); - font_button - } - - fn create_font_row(font_button: >k::FontDialogButton) -> gtk::Box { - let row = gtk::Box::new(gtk::Orientation::Horizontal, 10); - row.append(>k::Label::new(Some(&tr!("preferences.editor.font")))); - row.append(font_button); - row - } - - fn connect_signals( - sender: &Sender, - font_size_scale: >k::Scale, - soft_wrap_switch: >k::Switch, - custom_font_switch: >k::Switch, - font_button: >k::FontDialogButton, - ) { - let s = sender.clone(); - let fb = font_button.clone(); - font_size_scale.connect_value_changed(move |scale| { - s.emit(PreferencesInput::SetEditorFontSize(scale.value())); - Self::update_font_button_size(&fb, scale.value()); - }); - - let s = sender.clone(); - soft_wrap_switch.connect_state_set(move |_, state| { - s.emit(PreferencesInput::SetSoftWrapLines(state)); - gtk::glib::Propagation::Proceed - }); - - let s = sender.clone(); - let scale = font_size_scale.clone(); - font_button.connect_font_desc_notify(move |button| { - if let Some(font) = button.font_desc() { - if let Some(family) = font.family() { - s.emit(PreferencesInput::SetCustomEditorFontName( - family.to_string(), - )); - } - let size = font.size() / gtk::pango::SCALE; - if size > 0 { - scale.set_value(f64::from(size)); - } - } - }); - - let s = sender.clone(); - let fb = font_button.clone(); - custom_font_switch.connect_state_set(move |_, state| { - fb.set_sensitive(state); - s.emit(PreferencesInput::SetUseCustomEditorFont(state)); - gtk::glib::Propagation::Proceed - }); - } - - fn update_font_button_size(font_button: >k::FontDialogButton, size: f64) { - if let Some(mut font_desc) = font_button.font_desc() { - #[allow(clippy::cast_possible_truncation)] - font_desc.set_size(size as i32 * gtk::pango::SCALE); - font_button.set_font_desc(&font_desc); - } - } - - pub fn widget(&self) -> gtk::Box { - self.container.clone() - } - - pub fn sync_from(&self, preferences: &UserPreferences) { - self.font_size_scale.set_value(preferences.editor_font_size); - self.soft_wrap_switch - .set_active(preferences.soft_wrap_lines); - self.custom_font_switch - .set_active(preferences.use_custom_editor_font); - self.font_button - .set_sensitive(preferences.use_custom_editor_font); - #[allow(clippy::cast_possible_truncation)] - let font_desc = gtk::pango::FontDescription::from_string(&format!( - "{} {}", - preferences.custom_editor_font_name, preferences.editor_font_size as i32 - )); - self.font_button.set_font_desc(&font_desc); - } -} diff --git a/src/preferences/pages/general.rs b/src/preferences/pages/general.rs deleted file mode 100644 index e65ede6..0000000 --- a/src/preferences/pages/general.rs +++ /dev/null @@ -1,144 +0,0 @@ -// 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 . - -use gtk::FileFilter; -use gtk::prelude::*; -use relm4::Sender; -use relm4::{adw, gtk}; - -use crate::tr; - -use super::super::window::PreferencesInput; - -pub struct GeneralPage { - container: gtk::Box, -} - -impl GeneralPage { - pub fn new(sender: &Sender) -> Self { - let container = gtk::Box::new(gtk::Orientation::Vertical, 20); - container.set_margin_top(20); - container.set_margin_bottom(20); - container.set_margin_start(20); - container.set_margin_end(20); - container.set_valign(gtk::Align::Start); - - // Preferences section - let prefs_label = gtk::Label::new(Some(&tr!("preferences.general.preferences.title"))); - prefs_label.add_css_class("heading"); - prefs_label.set_halign(gtk::Align::Start); - container.append(&prefs_label); - - let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 10); - - // Export button - let export_button = gtk::Button::with_label(&tr!("preferences.general.preferences.export")); - { - let sender = sender.clone(); - export_button.connect_clicked(move |_| { - sender.emit(PreferencesInput::Export); - }); - } - button_box.append(&export_button); - - // Import button - let import_button = gtk::Button::with_label(&tr!("preferences.general.preferences.import")); - { - let sender = sender.clone(); - import_button.connect_clicked(move |_| { - sender.emit(PreferencesInput::Import); - }); - } - button_box.append(&import_button); - - // Reset button - let reset_button = gtk::Button::with_label(&tr!("preferences.general.preferences.reset")); - reset_button.add_css_class("destructive-action"); - { - let sender = sender.clone(); - reset_button.connect_clicked(move |_| { - sender.emit(PreferencesInput::ResetToDefaults); - }); - } - button_box.append(&reset_button); - - container.append(&button_box); - - // Help text - let help_label = gtk::Label::new(Some(&tr!("preferences.general.explanation"))); - help_label.add_css_class("dim-label"); - help_label.set_halign(gtk::Align::Start); - help_label.set_wrap(true); - container.append(&help_label); - - Self { container } - } - - pub fn widget(&self) -> gtk::Box { - self.container.clone() - } -} - -pub fn show_export_dialog(sender: &Sender, 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::(); - 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.emit(PreferencesInput::ExportToFile(path)); - } - }); -} - -pub fn show_import_dialog(sender: &Sender, 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::(); - 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.emit(PreferencesInput::ImportFromFile(path)); - } - }); -} diff --git a/src/preferences/pages/map.rs b/src/preferences/pages/map.rs deleted file mode 100644 index 5645a59..0000000 --- a/src/preferences/pages/map.rs +++ /dev/null @@ -1,212 +0,0 @@ -// 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 . - -use relm4::Sender; -use relm4::gtk; -use sourceview5::prelude::*; - -use crate::tr; -use crate::ui_helpers::create_switch_row; - -use super::super::models::UserPreferences; -use super::super::window::PreferencesInput; - -pub struct MapPage { - container: gtk::Box, - show_background_switch: gtk::Switch, - smart_positioning_switch: gtk::Switch, - custom_font_switch: gtk::Switch, - font_button: gtk::FontDialogButton, - export_format_dropdown: gtk::DropDown, -} - -impl MapPage { - pub fn new(preferences: &UserPreferences, sender: &Sender) -> Self { - let container = gtk::Box::new(gtk::Orientation::Vertical, 20); - container.set_margin_top(20); - container.set_margin_bottom(20); - container.set_margin_start(20); - container.set_margin_end(20); - container.set_valign(gtk::Align::Start); - - let (show_background_switch, smart_positioning_switch, custom_font_switch, font_button) = - Self::init_style_section(&container, preferences, sender); - - let export_format_dropdown = Self::init_export_section(&container, preferences, sender); - - Self { - container, - show_background_switch, - smart_positioning_switch, - custom_font_switch, - font_button, - export_format_dropdown, - } - } - - fn init_style_section( - container: >k::Box, - preferences: &UserPreferences, - sender: &Sender, - ) -> (gtk::Switch, gtk::Switch, gtk::Switch, gtk::FontDialogButton) { - let style_label = gtk::Label::new(Some(&tr!("preferences.map.map_style.title"))); - style_label.add_css_class("heading"); - style_label.set_halign(gtk::Align::Start); - container.append(&style_label); - - // Show Background - let background_row = create_switch_row( - &tr!("preferences.map.map_style.show_background"), - preferences.show_map_background, - ); - let show_background_switch = background_row.1.clone(); - { - let sender = sender.clone(); - show_background_switch.connect_state_set(move |_, state| { - sender.emit(PreferencesInput::SetShowMapBackground(state)); - gtk::glib::Propagation::Proceed - }); - } - container.append(&background_row.0); - - // Smart Label Positioning - let smart_row = create_switch_row( - &tr!("preferences.map.map_style.use_smart_label_positioning"), - preferences.use_smart_label_positioning, - ); - let smart_positioning_switch = smart_row.1.clone(); - { - let sender = sender.clone(); - smart_positioning_switch.connect_state_set(move |_, state| { - sender.emit(PreferencesInput::SetUseSmartLabelPositioning(state)); - gtk::glib::Propagation::Proceed - }); - } - container.append(&smart_row.0); - - // Use Custom Font - let custom_font_row = create_switch_row( - &tr!("preferences.map.map_style.use_custom_font"), - preferences.use_custom_font, - ); - let custom_font_switch = custom_font_row.1.clone(); - container.append(&custom_font_row.0); - - // Font selection - let font_row = gtk::Box::new(gtk::Orientation::Horizontal, 10); - font_row.append(>k::Label::new(Some(&tr!("preferences.map.font")))); - - let font_dialog = gtk::FontDialog::new(); - let font_button = gtk::FontDialogButton::new(Some(font_dialog)); - let font_desc = gtk::pango::FontDescription::from_string(&preferences.custom_font_name); - font_button.set_font_desc(&font_desc); - font_button.set_sensitive(preferences.use_custom_font); - font_button.set_hexpand(true); - font_button.set_use_font(true); - { - let sender = sender.clone(); - font_button.connect_font_desc_notify(move |button| { - if let Some(font) = button.font_desc() - && let Some(family) = font.family() - { - sender.emit(PreferencesInput::SetCustomFontName(family.to_string())); - } - }); - } - font_row.append(&font_button); - container.append(&font_row); - - // Connect custom font switch to font button sensitivity - { - let font_button_clone = font_button.clone(); - let sender = sender.clone(); - custom_font_switch.connect_state_set(move |_, state| { - font_button_clone.set_sensitive(state); - sender.emit(PreferencesInput::SetUseCustomFont(state)); - gtk::glib::Propagation::Proceed - }); - } - - ( - show_background_switch, - smart_positioning_switch, - custom_font_switch, - font_button, - ) - } - - fn init_export_section( - container: >k::Box, - preferences: &UserPreferences, - sender: &Sender, - ) -> gtk::DropDown { - let export_label = gtk::Label::new(Some(&tr!("preferences.map.export.title"))); - export_label.add_css_class("heading"); - export_label.set_halign(gtk::Align::Start); - export_label.set_margin_top(10); - container.append(&export_label); - - let export_row = gtk::Box::new(gtk::Orientation::Horizontal, 10); - export_row.append(>k::Label::new(Some(&tr!( - "preferences.map.export.default_format" - )))); - - let formats = - gtk::StringList::new(&[&tr!("export_formats.png"), &tr!("export_formats.svg")]); - let export_format_dropdown = gtk::DropDown::new(Some(formats), None::); - let selected = match preferences.default_export_format.as_str() { - "svg" => 1, - _ => 0, - }; - export_format_dropdown.set_selected(selected); - { - let sender = sender.clone(); - export_format_dropdown.connect_selected_notify(move |dropdown| { - let format = match dropdown.selected() { - 1 => "svg", - _ => "png", - }; - sender.emit(PreferencesInput::SetDefaultExportFormat(format.to_string())); - }); - } - export_row.append(&export_format_dropdown); - container.append(&export_row); - - export_format_dropdown - } - - pub fn widget(&self) -> gtk::Box { - self.container.clone() - } - - pub fn sync_from(&self, preferences: &UserPreferences) { - self.show_background_switch - .set_active(preferences.show_map_background); - self.smart_positioning_switch - .set_active(preferences.use_smart_label_positioning); - self.custom_font_switch - .set_active(preferences.use_custom_font); - self.font_button.set_sensitive(preferences.use_custom_font); - let font_desc = gtk::pango::FontDescription::from_string(&preferences.custom_font_name); - self.font_button.set_font_desc(&font_desc); - - let selected = match preferences.default_export_format.as_str() { - "svg" => 1, - _ => 0, - }; - self.export_format_dropdown.set_selected(selected); - } -} diff --git a/src/preferences/pages/mod.rs b/src/preferences/pages/mod.rs deleted file mode 100644 index 2ace921..0000000 --- a/src/preferences/pages/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 . - -pub mod editor; -pub mod general; -pub mod map; -pub mod stages; -pub mod templates; diff --git a/src/preferences/pages/stages.rs b/src/preferences/pages/stages.rs deleted file mode 100644 index 4bd24e1..0000000 --- a/src/preferences/pages/stages.rs +++ /dev/null @@ -1,253 +0,0 @@ -// 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 . - -use gtk::prelude::*; -use relm4::Sender; -use relm4::gtk; -use uuid::Uuid; - -use crate::tr; - -use super::super::models::UserPreferences; -use super::super::window::{PreferencesInput, StageLabel}; - -pub struct StagesPage { - container: gtk::Box, - list_box: gtk::ListBox, - sender: Sender, -} - -impl StagesPage { - pub fn new(preferences: &UserPreferences, sender: Sender) -> Self { - let container = gtk::Box::new(gtk::Orientation::Vertical, 10); - container.set_margin_top(20); - container.set_margin_bottom(20); - container.set_margin_start(20); - container.set_margin_end(20); - - // Header - let header_label = gtk::Label::new(Some(&tr!("preferences.stages.title"))); - header_label.add_css_class("heading"); - header_label.set_halign(gtk::Align::Start); - container.append(&header_label); - - let help_label = gtk::Label::new(Some(&tr!("preferences.stages.explanation"))); - help_label.add_css_class("dim-label"); - help_label.set_halign(gtk::Align::Start); - help_label.set_wrap(true); - help_label.set_margin_bottom(10); - container.append(&help_label); - - // Column headers - let header_row = gtk::Box::new(gtk::Orientation::Horizontal, 5); - header_row.add_css_class("dim-label"); - - let name_header = gtk::Label::new(Some(&tr!("preferences.stages.column.name"))); - name_header.set_width_chars(15); - name_header.set_xalign(0.0); - header_row.append(&name_header); - - let first_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_i"))); - first_header.set_width_chars(12); - first_header.set_xalign(0.0); - header_row.append(&first_header); - - let second_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_ii"))); - second_header.set_width_chars(12); - second_header.set_xalign(0.0); - header_row.append(&second_header); - - let third_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_iii"))); - third_header.set_width_chars(12); - third_header.set_xalign(0.0); - header_row.append(&third_header); - - let fourth_header = gtk::Label::new(Some(&tr!("preferences.stages.column.stage_iv"))); - fourth_header.set_width_chars(12); - fourth_header.set_xalign(0.0); - header_row.append(&fourth_header); - - // Spacer for delete button column - let spacer = gtk::Box::new(gtk::Orientation::Horizontal, 0); - spacer.set_width_request(32); - header_row.append(&spacer); - - container.append(&header_row); - - // Scrollable list area - let scrolled = gtk::ScrolledWindow::new(); - scrolled.set_vexpand(true); - scrolled.set_min_content_height(200); - - let list_box = gtk::ListBox::new(); - list_box.set_selection_mode(gtk::SelectionMode::None); - list_box.add_css_class("boxed-list"); - - scrolled.set_child(Some(&list_box)); - container.append(&scrolled); - - // Add button - let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 10); - button_box.set_halign(gtk::Align::End); - - let add_button = gtk::Button::from_icon_name("list-add-symbolic"); - add_button.set_tooltip_text(Some(&tr!("preferences.stages.help.add"))); - { - let sender = sender.clone(); - add_button.connect_clicked(move |_| { - sender.emit(PreferencesInput::AddCustomStage); - }); - } - button_box.append(&add_button); - container.append(&button_box); - - let page = Self { - container, - list_box, - sender, - }; - - page.populate_stages(preferences); - page - } - - pub fn widget(&self) -> gtk::Box { - self.container.clone() - } - - pub fn refresh(&self, preferences: &UserPreferences) { - // Clear existing rows - while let Some(child) = self.list_box.first_child() { - self.list_box.remove(&child); - } - self.populate_stages(preferences); - } - - fn populate_stages(&self, preferences: &UserPreferences) { - for stage in &preferences.custom_stages { - let row = self.create_stage_row(stage.id, &stage.name, &stage.stage); - self.list_box.append(&row); - } - } - - fn create_stage_row( - &self, - id: Uuid, - name: &str, - stage: &super::super::models::Stage, - ) -> gtk::ListBoxRow { - let row = gtk::ListBoxRow::new(); - let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5); - hbox.set_margin_top(5); - hbox.set_margin_bottom(5); - hbox.set_margin_start(5); - hbox.set_margin_end(5); - - // Name entry - let name_entry = gtk::Entry::new(); - name_entry.set_text(name); - name_entry.set_width_chars(15); - { - let sender = self.sender.clone(); - name_entry.connect_changed(move |entry| { - sender.emit(PreferencesInput::SetCustomStageName( - id, - entry.text().to_string(), - )); - }); - } - hbox.append(&name_entry); - - // Stage I entry - let first_entry = gtk::Entry::new(); - first_entry.set_text(&stage.i); - first_entry.set_width_chars(12); - { - let sender = self.sender.clone(); - first_entry.connect_changed(move |entry| { - sender.emit(PreferencesInput::SetCustomStageLabel( - id, - StageLabel::I, - entry.text().to_string(), - )); - }); - } - hbox.append(&first_entry); - - // Stage II entry - let second_entry = gtk::Entry::new(); - second_entry.set_text(&stage.ii); - second_entry.set_width_chars(12); - { - let sender = self.sender.clone(); - second_entry.connect_changed(move |entry| { - sender.emit(PreferencesInput::SetCustomStageLabel( - id, - StageLabel::Ii, - entry.text().to_string(), - )); - }); - } - hbox.append(&second_entry); - - // Stage III entry - let third_entry = gtk::Entry::new(); - third_entry.set_text(&stage.iii); - third_entry.set_width_chars(12); - { - let sender = self.sender.clone(); - third_entry.connect_changed(move |entry| { - sender.emit(PreferencesInput::SetCustomStageLabel( - id, - StageLabel::Iii, - entry.text().to_string(), - )); - }); - } - hbox.append(&third_entry); - - // Stage IV entry - let fourth_entry = gtk::Entry::new(); - fourth_entry.set_text(&stage.iv); - fourth_entry.set_width_chars(12); - { - let sender = self.sender.clone(); - fourth_entry.connect_changed(move |entry| { - sender.emit(PreferencesInput::SetCustomStageLabel( - id, - StageLabel::Iv, - entry.text().to_string(), - )); - }); - } - hbox.append(&fourth_entry); - - // Delete button - let delete_button = gtk::Button::from_icon_name("list-remove-symbolic"); - delete_button.add_css_class("flat"); - delete_button.set_tooltip_text(Some(&tr!("preferences.stages.help.remove"))); - { - let sender = self.sender.clone(); - delete_button.connect_clicked(move |_| { - sender.emit(PreferencesInput::RemoveCustomStage(id)); - }); - } - hbox.append(&delete_button); - - row.set_child(Some(&hbox)); - row - } -} diff --git a/src/preferences/pages/templates.rs b/src/preferences/pages/templates.rs deleted file mode 100644 index 029b3e9..0000000 --- a/src/preferences/pages/templates.rs +++ /dev/null @@ -1,349 +0,0 @@ -// 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 . - -use std::cell::RefCell; -use std::rc::Rc; - -use relm4::Sender; -use relm4::gtk; -use sourceview5::prelude::*; -use uuid::Uuid; - -use crate::tr; - -use super::super::models::{Template, UserPreferences}; -use super::super::window::PreferencesInput; - -pub struct TemplatesPage { - container: gtk::Box, - list_box: gtk::ListBox, - #[allow(dead_code)] - editor: sourceview5::View, - editor_buffer: sourceview5::Buffer, - selected_id: Rc>>, - sender: Sender, -} - -impl TemplatesPage { - pub fn new(prefs: &UserPreferences, sender: Sender) -> Self { - let container = gtk::Box::new(gtk::Orientation::Horizontal, 0); - container.set_margin_top(10); - container.set_margin_bottom(10); - container.set_margin_start(10); - container.set_margin_end(10); - - let selected_id: Rc>> = Rc::new(RefCell::new(None)); - - // Left sidebar - let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 5); - sidebar.set_width_request(200); - - let sidebar_label = gtk::Label::new(Some(&tr!("preferences.templates.title"))); - sidebar_label.add_css_class("heading"); - sidebar_label.set_halign(gtk::Align::Start); - sidebar_label.set_margin_start(5); - sidebar.append(&sidebar_label); - - // Template list - let scrolled = gtk::ScrolledWindow::new(); - scrolled.set_vexpand(true); - - let list_box = gtk::ListBox::new(); - list_box.set_selection_mode(gtk::SelectionMode::Single); - list_box.add_css_class("boxed-list"); - - scrolled.set_child(Some(&list_box)); - sidebar.append(&scrolled); - - // Sidebar buttons - let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 5); - button_box.set_halign(gtk::Align::End); - button_box.set_margin_top(5); - - let add_button = gtk::Button::from_icon_name("list-add-symbolic"); - add_button.set_tooltip_text(Some(&tr!("preferences.templates.help.add"))); - { - let sender = sender.clone(); - add_button.connect_clicked(move |_| { - sender.emit(PreferencesInput::AddTemplate(tr!( - "dialog.template.default_label" - ))); - }); - } - button_box.append(&add_button); - - let remove_button = gtk::Button::from_icon_name("list-remove-symbolic"); - remove_button.set_tooltip_text(Some(&tr!("preferences.templates.help.remove"))); - { - let sender = sender.clone(); - let selected_id = selected_id.clone(); - remove_button.connect_clicked(move |_| { - if let Some(id) = *selected_id.borrow() { - sender.emit(PreferencesInput::RemoveTemplate(id)); - } - }); - } - button_box.append(&remove_button); - - sidebar.append(&button_box); - container.append(&sidebar); - - // Separator - let separator = gtk::Separator::new(gtk::Orientation::Vertical); - separator.set_margin_start(10); - separator.set_margin_end(10); - container.append(&separator); - - // Right side: editor - let editor_box = gtk::Box::new(gtk::Orientation::Vertical, 5); - editor_box.set_hexpand(true); - - let editor_label = gtk::Label::new(Some(&tr!("preferences.templates.content"))); - editor_label.add_css_class("heading"); - editor_label.set_halign(gtk::Align::Start); - editor_box.append(&editor_label); - - let editor_scrolled = gtk::ScrolledWindow::new(); - editor_scrolled.set_vexpand(true); - editor_scrolled.set_hexpand(true); - - let editor_buffer = sourceview5::Buffer::new(None); - editor_buffer.set_highlight_syntax(true); - - // Try to set wmap language - let language_manager = sourceview5::LanguageManager::default(); - if let Some(language) = language_manager.language("wmap") { - editor_buffer.set_language(Some(&language)); - } - - let editor = sourceview5::View::with_buffer(&editor_buffer); - editor.set_monospace(true); - editor.set_show_line_numbers(true); - editor.set_tab_width(2); - - // Connect buffer changes to save - { - let sender = sender.clone(); - let selected_id = selected_id.clone(); - editor_buffer.connect_changed(move |buffer| { - if let Some(id) = *selected_id.borrow() { - let start = buffer.start_iter(); - let end = buffer.end_iter(); - let content = buffer.text(&start, &end, false).to_string(); - sender.emit(PreferencesInput::SetTemplateContent(id, content)); - } - }); - } - - editor_scrolled.set_child(Some(&editor)); - editor_box.append(&editor_scrolled); - container.append(&editor_box); - - let page = Self { - container, - list_box, - editor, - editor_buffer, - selected_id, - sender, - }; - - page.populate_templates(prefs); - - // Select first template if available - if let Some(first) = prefs.map_templates.first() { - *page.selected_id.borrow_mut() = Some(first.id); - page.editor_buffer.set_text(&first.content); - } - - page - } - - pub fn widget(&self) -> gtk::Box { - self.container.clone() - } - - pub fn refresh(&self, prefs: &UserPreferences) { - // Check if we need to recreate rows (templates added/removed/reordered) - let current_row_count = { - let mut count = 0; - let mut child = self.list_box.first_child(); - while child.is_some() { - count += 1; - child = child.and_then(|c| c.next_sibling()); - } - count - }; - - let needs_recreate = current_row_count != prefs.map_templates.len(); - - if needs_recreate { - // Clear existing rows - while let Some(child) = self.list_box.first_child() { - self.list_box.remove(&child); - } - self.populate_templates(prefs); - } else { - // Just update existing rows in place - let mut child = self.list_box.first_child(); - let mut index = 0; - while let Some(row) = child { - if let Some(template) = prefs.map_templates.get(index) { - TemplatesPage::update_template_row(&row, template); - } - child = row.next_sibling(); - index += 1; - } - } - - // Update editor if selected template still exists - let selected = *self.selected_id.borrow(); - if let Some(id) = selected { - if let Some(template) = prefs.map_templates.iter().find(|t| t.id == id) { - // Only update if content differs to avoid cursor jumping - let current = { - let start = self.editor_buffer.start_iter(); - let end = self.editor_buffer.end_iter(); - self.editor_buffer.text(&start, &end, false).to_string() - }; - if current != template.content { - self.editor_buffer.set_text(&template.content); - } - } else { - // Selected template was deleted, select first - if let Some(first) = prefs.map_templates.first() { - *self.selected_id.borrow_mut() = Some(first.id); - self.editor_buffer.set_text(&first.content); - } else { - *self.selected_id.borrow_mut() = None; - self.editor_buffer.set_text(""); - } - } - } - } - - fn populate_templates(&self, prefs: &UserPreferences) { - for template in &prefs.map_templates { - let row = self.create_template_row(template); - self.list_box.append(&row); - } - - // Connect selection changed - { - let selected_id = self.selected_id.clone(); - let editor_buffer = self.editor_buffer.clone(); - let _sender = self.sender.clone(); - - // We need to store templates for the closure - let templates: Vec