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