// 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 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("Templates"));
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("Add template"));
{
let sender = sender.clone();
add_button.connect_clicked(move |_| {
// Show a simple dialog to get the template name
show_add_template_dialog(sender.clone());
});
}
button_box.append(&add_button);
let remove_button = gtk::Button::from_icon_name("list-remove-symbolic");
remove_button.set_tooltip_text(Some("Remove template"));
{
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("Template 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) {
// Clear existing rows
while let Some(child) = self.list_box.first_child() {
self.list_box.remove(&child);
}
self.populate_templates(prefs);
// 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 = prefs.map_templates.clone();
self.list_box.connect_row_selected(move |_, row| {
if let Some(row) = row {
let index = row.index() as usize;
if let Some(template) = templates.get(index) {
*selected_id.borrow_mut() = Some(template.id);
editor_buffer.set_text(&template.content);
}
}
});
}
}
fn create_template_row(&self, template: &Template) -> gtk::ListBoxRow {
let row = gtk::ListBoxRow::new();
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 10);
hbox.set_margin_top(8);
hbox.set_margin_bottom(8);
hbox.set_margin_start(8);
hbox.set_margin_end(8);
// Default indicator button
let default_button = gtk::Button::new();
if template.is_default {
default_button.set_icon_name("emblem-default-symbolic");
default_button.set_tooltip_text(Some("Default template"));
} else {
default_button.set_icon_name("radio-symbolic");
default_button.set_tooltip_text(Some("Set as default"));
}
default_button.add_css_class("flat");
{
let sender = self.sender.clone();
let id = template.id;
default_button.connect_clicked(move |_| {
sender.emit(PreferencesInput::SetDefaultTemplate(id));
});
}
hbox.append(&default_button);
// Template name
let name_label = gtk::Label::new(Some(&template.name));
name_label.set_hexpand(true);
name_label.set_xalign(0.0);
hbox.append(&name_label);
row.set_child(Some(&hbox));
row
}
}
fn show_add_template_dialog(sender: Sender) {
let dialog = gtk::Dialog::with_buttons(
Some("New Template"),
None::<>k::Window>,
gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT,
&[
("Cancel", gtk::ResponseType::Cancel),
("Add", gtk::ResponseType::Accept),
],
);
dialog.set_default_response(gtk::ResponseType::Accept);
let content = dialog.content_area();
content.set_margin_top(10);
content.set_margin_bottom(10);
content.set_margin_start(10);
content.set_margin_end(10);
content.set_spacing(10);
let label = gtk::Label::new(Some("Template name:"));
label.set_halign(gtk::Align::Start);
content.append(&label);
let entry = gtk::Entry::new();
entry.set_placeholder_text(Some("My Template"));
entry.set_activates_default(true);
content.append(&entry);
dialog.connect_response(move |dialog, response| {
if response == gtk::ResponseType::Accept {
let name = entry.text().to_string();
if !name.is_empty() {
sender.emit(PreferencesInput::AddTemplate(name));
}
}
dialog.close();
});
dialog.present();
}