aboutsummaryrefslogtreecommitdiff
path: root/src/components/preference_pages/templates.rs
diff options
context:
space:
mode:
authorRubén Beltrán del Río <jj@r.bdr.sh>2026-03-27 16:10:46 +0100
committerRubén Beltrán del Río <jj@r.bdr.sh>2026-03-30 13:39:14 +0200
commitd01a4df546b553128400f6c68eefa8368b4113a8 (patch)
treed290ca78b1215fb450ec7850a74b79fd0ae9707c /src/components/preference_pages/templates.rs
parent4dec344a7bcbf1d7d0bbbf985109df816470ecc6 (diff)
Use actual components for preference pages
Diffstat (limited to 'src/components/preference_pages/templates.rs')
-rw-r--r--src/components/preference_pages/templates.rs443
1 files changed, 443 insertions, 0 deletions
diff --git a/src/components/preference_pages/templates.rs b/src/components/preference_pages/templates.rs
new file mode 100644
index 0000000..5652816
--- /dev/null
+++ b/src/components/preference_pages/templates.rs
@@ -0,0 +1,443 @@
+// 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 adw::prelude::*;
+use relm4::prelude::*;
+use sourceview5::LanguageManager;
+use sourceview5::prelude::*;
+use uuid::Uuid;
+
+use crate::tr;
+
+use crate::actions::PreferencesAction;
+use crate::components::template_row;
+use crate::preferences::models::Template;
+
+pub struct TemplatesInit {
+ pub templates: Vec<Template>,
+ pub window: adw::Window,
+}
+
+pub struct Templates {
+ template_rows: FactoryVecDeque<template_row::TemplateRow>,
+ selected_template: Option<Template>,
+ selected_name: String,
+ source_view: sourceview5::View,
+ source: sourceview5::Buffer,
+ is_collapsed: bool,
+ is_showing_content: bool,
+}
+
+#[derive(Debug, Clone)]
+pub enum TemplatesAction {
+ TemplateAdded(Template),
+ TemplateUpdated(Template),
+ TemplateRemoved(Uuid),
+ UpdateAllTemplates(Vec<Template>),
+
+ // Routed Via Outputs
+ ChangeName(DynamicIndex, String),
+ ChangeDefaultTemplate(DynamicIndex),
+ Activate(DynamicIndex),
+ Focus(DynamicIndex),
+
+ // Internal
+ Deactivate,
+ SelectTemplate(usize),
+ RemoveTemplate,
+ SourceChanged(String),
+ Collapse,
+ Expand,
+}
+
+#[relm4::component(pub)]
+impl SimpleComponent for Templates {
+ type Init = TemplatesInit;
+ type Input = TemplatesAction;
+ type Output = PreferencesAction;
+ view! {
+ #[name = "split_view"]
+ adw::NavigationSplitView {
+ set_collapsed: true,
+ #[watch]
+ set_show_content: model.is_showing_content,
+
+ connect_show_content_notify[sender] => move |nav| {
+ if !nav.shows_content() {
+ sender.input(TemplatesAction::Deactivate);
+ }
+ },
+
+ #[wrap(Some)]
+ set_sidebar = &adw::NavigationPage {
+ set_title: &tr!("preferences.templates.title"),
+ gtk::Box {
+ set_orientation: gtk::Orientation::Vertical,
+ set_margin_all: 20,
+ set_spacing: 10,
+
+ gtk::Label {
+ set_label: &tr!("preferences.templates.title"),
+ add_css_class: "heading",
+ set_halign: gtk::Align::Start,
+ set_margin_start: 5,
+ },
+
+ model.template_rows.widget() -> &gtk::ListBox {
+ set_selection_mode: gtk::SelectionMode::Single,
+ add_css_class: "boxed-list",
+ connect_row_selected[sender] => move |_, row| {
+ if let Some(row) = row {
+ let index = row.index() as usize;
+ sender.input(TemplatesAction::SelectTemplate(index));
+ }
+ }
+ },
+
+ gtk::Box {
+ set_spacing: 5,
+ set_halign: gtk::Align::End,
+
+ gtk::Button::from_icon_name("list-remove-symbolic") {
+ set_tooltip_text: Some(&tr!("preferences.templates.help.remove")),
+
+ connect_clicked[sender] => move |_| {
+ sender.input(TemplatesAction::RemoveTemplate);
+ }
+ },
+
+ gtk::Button::from_icon_name("list-add-symbolic") {
+ set_tooltip_text: Some(&tr!("preferences.templates.help.add")),
+
+ connect_clicked[sender] => move |_| {
+ sender.output(PreferencesAction::AddTemplate(tr!(
+ "dialog.template.default_label"
+ ))).ok();
+ },
+ },
+ },
+ },
+ },
+
+ #[wrap(Some)]
+ set_content = &adw::NavigationPage {
+ set_title: &model.selected_name,
+ gtk::Box {
+ set_orientation: gtk::Orientation::Vertical,
+ set_spacing: 0,
+
+ adw::HeaderBar {
+ set_show_end_title_buttons: false,
+ set_show_start_title_buttons: false,
+ #[wrap(Some)]
+ set_title_widget = if model.template_rows.len() > 0 || !model.selected_template.is_some() {
+ &gtk::Label {
+ #[watch]
+ set_label: &model.selected_name,
+ add_css_class: "heading",
+ set_hexpand: true,
+ set_halign: gtk::Align::Start,
+ }
+ } else {
+ &gtk::Label {
+ #[watch]
+ set_label: "",
+ add_css_class: "heading",
+ set_hexpand: true,
+ set_halign: gtk::Align::Start,
+ }
+ }
+ },
+
+ gtk::Box {
+ set_orientation: gtk::Orientation::Vertical,
+ set_spacing: 5,
+ set_hexpand: true,
+
+ if model.template_rows.len() > 0 && model.selected_template.is_some() {
+ gtk::ScrolledWindow {
+ set_vexpand: true,
+ set_hexpand: true,
+ #[local_ref]
+ source_view -> sourceview5::View {
+ set_expand: true,
+ set_buffer: Some(&model.source),
+ set_top_margin: 8,
+ set_bottom_margin: 8,
+ set_left_margin: 8,
+ set_right_margin: 8
+ }
+ }
+ } else {
+ gtk::Label {
+ set_label: &tr!("preferences.templates.empty"),
+ add_css_class: "dim-label",
+ set_hexpand: true,
+ set_vexpand: true,
+ }
+ }
+
+ },
+ },
+ },
+ }
+ }
+
+ fn init(
+ init: Self::Init,
+ root: Self::Root,
+ sender: ComponentSender<Self>,
+ ) -> ComponentParts<Self> {
+ let mut template_rows = FactoryVecDeque::builder()
+ .launch(gtk::ListBox::default())
+ .forward(sender.input_sender(), |output| match output {
+ template_row::TemplateRowOutput::SetName(index, name) => {
+ TemplatesAction::ChangeName(index, name)
+ }
+ template_row::TemplateRowOutput::SetDefault(index) => {
+ TemplatesAction::ChangeDefaultTemplate(index)
+ }
+ template_row::TemplateRowOutput::Activate(index) => {
+ TemplatesAction::Activate(index)
+ }
+ template_row::TemplateRowOutput::Focus(index) => TemplatesAction::Focus(index),
+ });
+
+ let first = init.templates.first();
+ let selected_template = first.map_or(None, |t| Some(t.clone()));
+ let source = Self::create_source_buffer(first, &sender);
+
+ {
+ let mut guard = template_rows.guard();
+ for template in init.templates {
+ guard.push_back(template);
+ }
+ }
+
+ let source_view = Self::init_source_view(&source);
+ let selected_name = selected_template.clone().map_or("".to_string(), |t| t.name);
+
+ let model = Self {
+ selected_template,
+ selected_name,
+ template_rows,
+ source_view: source_view.clone(),
+ source,
+ is_collapsed: false,
+ is_showing_content: false,
+ };
+
+ let widgets = view_output!();
+ Self::init_breakpoints(&init.window, &widgets, &sender);
+ ComponentParts { model, widgets }
+ }
+
+ fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
+ match message {
+ TemplatesAction::TemplateAdded(template) => {
+ self.template_rows.guard().push_back(template);
+ }
+ TemplatesAction::TemplateUpdated(template) => {
+ if let Some(row) = self
+ .template_rows
+ .guard()
+ .iter_mut()
+ .find(|s| s.template.id == template.id)
+ {
+ row.template.name = template.name;
+ row.template.is_default = template.is_default;
+ row.template.content = template.content;
+ }
+ }
+ TemplatesAction::TemplateRemoved(id) => {
+ let mut guard = self.template_rows.guard();
+ let index = guard.iter().position(|s| s.template.id == id);
+ if let Some(index) = index {
+ guard.remove(index);
+ if self
+ .selected_template
+ .as_ref()
+ .map_or(false, |t| id == t.id)
+ {
+ guard.drop();
+ self.select_template(0, &sender);
+ }
+ }
+ }
+ TemplatesAction::RemoveTemplate => {
+ if let Some(template) = &self.selected_template {
+ sender
+ .output(PreferencesAction::RemoveTemplate(template.id))
+ .ok();
+ }
+ }
+ TemplatesAction::UpdateAllTemplates(templates) => {
+ let mut guard = self.template_rows.guard();
+ guard.clear();
+ for template in templates {
+ guard.push_back(template);
+ }
+ guard.drop();
+ let index = if let Some(template) = &self.selected_template {
+ self.template_rows
+ .guard()
+ .iter()
+ .position(|s| s.template.id == template.id)
+ .map_or(0, |i| i)
+ } else {
+ 0
+ };
+ self.select_template(index, &sender);
+ }
+
+ TemplatesAction::ChangeName(index, name) => {
+ if let Some(row) = self.template_rows.get(index.current_index()) {
+ sender
+ .output(PreferencesAction::SetTemplateName(row.template.id, name))
+ .ok();
+ }
+ }
+ TemplatesAction::ChangeDefaultTemplate(index) => {
+ let index = index.current_index();
+ if let Some(row) = self.template_rows.get(index) {
+ sender
+ .output(PreferencesAction::SetDefaultTemplate(row.template.id))
+ .ok();
+ }
+ self.select_template(index, &sender);
+ }
+ TemplatesAction::Activate(index) => {
+ self.select_template(index.current_index(), &sender);
+ self.is_showing_content = true;
+ }
+ TemplatesAction::Deactivate => {
+ self.is_showing_content = false;
+ }
+ TemplatesAction::Focus(index) => {
+ self.select_template(index.current_index(), &sender);
+ }
+ TemplatesAction::SelectTemplate(index) => {
+ self.select_template(index, &sender);
+ }
+ TemplatesAction::SourceChanged(source) => {
+ if let Some(template) = &self.selected_template {
+ sender
+ .output(PreferencesAction::SetTemplateContent(template.id, source))
+ .ok();
+ }
+ }
+ TemplatesAction::Collapse => {
+ self.is_collapsed = true;
+ self.template_rows
+ .broadcast(template_row::TemplateRowAction::ShowArrow)
+ }
+ TemplatesAction::Expand => {
+ self.is_collapsed = false;
+ self.is_showing_content = false;
+ self.template_rows
+ .broadcast(template_row::TemplateRowAction::HideArrow)
+ }
+ }
+ }
+}
+
+impl Templates {
+ // Initializes the breakpoint logic
+ fn init_breakpoints(
+ root: &adw::Window,
+ widgets: &TemplatesWidgets,
+ sender: &ComponentSender<Self>,
+ ) {
+ let breakpoint = adw::Breakpoint::new(adw::BreakpointCondition::new_length(
+ adw::BreakpointConditionLengthType::MinWidth,
+ 550.0,
+ adw::LengthUnit::Sp,
+ ));
+ breakpoint.add_setter(&widgets.split_view, "collapsed", Some(&false.to_value()));
+ breakpoint.add_setter(
+ &widgets.source_view,
+ "show-line-numbers",
+ Some(&true.to_value()),
+ );
+ {
+ let sender = sender.clone();
+ breakpoint.connect_apply(move |_| {
+ sender.input(TemplatesAction::Expand);
+ });
+ }
+ {
+ let sender = sender.clone();
+ breakpoint.connect_unapply(move |_| {
+ sender.input(TemplatesAction::Collapse);
+ });
+ }
+ root.add_breakpoint(breakpoint);
+ }
+
+ fn create_source_buffer(
+ template: Option<&Template>,
+ sender: &ComponentSender<Self>,
+ ) -> sourceview5::Buffer {
+ let source = sourceview5::Buffer::new(None);
+ source.set_highlight_syntax(true);
+
+ let language_manager = LanguageManager::default();
+ if let Some(language) = language_manager.language("wmap") {
+ source.set_language(Some(&language));
+ }
+
+ if let Some(template) = template {
+ source.set_text(&template.content);
+ }
+
+ {
+ let sender = sender.clone();
+ source.connect_changed(move |buffer| {
+ let start = buffer.start_iter();
+ let end = buffer.end_iter();
+ let content = buffer.text(&start, &end, false).to_string();
+ sender.input(TemplatesAction::SourceChanged(content));
+ });
+ }
+
+ source
+ }
+
+ fn init_source_view(source: &sourceview5::Buffer) -> sourceview5::View {
+ let source_view = sourceview5::View::with_buffer(source);
+ source_view.set_monospace(true);
+ source_view.add_css_class("map-editor");
+ source_view.set_show_line_numbers(false);
+ source_view.set_width_request(-1);
+ source_view.set_tab_width(2);
+ source_view
+ }
+
+ fn select_template(&mut self, index: usize, sender: &ComponentSender<Self>) {
+ let guard = self.template_rows.guard();
+ if let Some(row) = guard.get(index) {
+ self.selected_template = Some(row.template.clone());
+ self.selected_name = row.template.clone().name;
+ self.source = Self::create_source_buffer(Some(&row.template), &sender);
+ self.source_view.set_buffer(Some(&self.source));
+ }
+ guard.drop();
+
+ let widget = self.template_rows.widget();
+ let potential_row_widget = widget.row_at_index(index as i32);
+ widget.select_row(potential_row_widget.as_ref());
+ }
+}