/*
GREX - A graphical frontend for creating and managing Rex project files.
Copyright (C) 2026 SILO GROUP, LLC. Written by Chris Punches
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 .
*/
#include "views/unit_editor.h"
#include "util/unit_picker.h"
#include "util/unit_properties_dialog.h"
namespace grex {
UnitEditor::UnitEditor(Project& project, GrexConfig& grex_config)
: project_(project), grex_config_(grex_config) {
root_ = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(root_), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
content_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_set_margin_start(content_box_, 16);
gtk_widget_set_margin_end(content_box_, 16);
gtk_widget_set_margin_top(content_box_, 16);
gtk_widget_set_margin_bottom(content_box_, 16);
auto* box = content_box_;
// Task properties container — disabled when no task selected
task_section_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
auto* task_label = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(task_label), "Task Properties");
gtk_label_set_xalign(GTK_LABEL(task_label), 0.0f);
gtk_box_append(GTK_BOX(task_section_), task_label);
auto* task_grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(task_grid), 6);
gtk_grid_set_column_spacing(GTK_GRID(task_grid), 12);
// Name (read-only label)
auto* name_label = gtk_label_new("Name");
gtk_label_set_xalign(GTK_LABEL(name_label), 1.0f);
gtk_grid_attach(GTK_GRID(task_grid), name_label, 0, 0, 1, 1);
name_display_ = gtk_label_new("");
gtk_label_set_xalign(GTK_LABEL(name_display_), 0.0f);
gtk_widget_set_hexpand(name_display_, TRUE);
gtk_grid_attach(GTK_GRID(task_grid), name_display_, 1, 0, 1, 1);
// Comment
auto* comment_label = gtk_label_new("Comment");
gtk_label_set_xalign(GTK_LABEL(comment_label), 1.0f);
gtk_grid_attach(GTK_GRID(task_grid), comment_label, 0, 1, 1, 1);
entry_comment_ = gtk_entry_new();
gtk_widget_set_hexpand(entry_comment_, TRUE);
gtk_grid_attach(GTK_GRID(task_grid), entry_comment_, 1, 1, 1, 1);
gtk_box_append(GTK_BOX(task_section_), task_grid);
gtk_box_append(GTK_BOX(box), task_section_);
// Underlying Unit controls — exposed for external placement
unit_controls_ = gtk_frame_new("Underlying Unit");
auto* unit_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(unit_btn_box, 8);
gtk_widget_set_margin_end(unit_btn_box, 8);
gtk_widget_set_margin_top(unit_btn_box, 8);
gtk_widget_set_margin_bottom(unit_btn_box, 8);
btn_select_unit_ = gtk_button_new_with_label("Change/Select Unit...");
g_signal_connect(btn_select_unit_, "clicked", G_CALLBACK(on_select_unit), this);
gtk_box_append(GTK_BOX(unit_btn_box), btn_select_unit_);
btn_edit_unit_ = gtk_button_new_with_label("Edit Unit...");
g_signal_connect(btn_edit_unit_, "clicked", G_CALLBACK(on_edit_unit), this);
gtk_box_append(GTK_BOX(unit_btn_box), btn_edit_unit_);
btn_save_unit_ = gtk_button_new_with_label("Save Unit");
g_signal_connect(btn_save_unit_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast(d);
self->save_current();
}), this);
gtk_box_append(GTK_BOX(unit_btn_box), btn_save_unit_);
gtk_frame_set_child(GTK_FRAME(unit_controls_), unit_btn_box);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), box);
clear();
}
void UnitEditor::mark_dirty() {
dirty_ = true;
gtk_widget_add_css_class(btn_save_unit_, "suggested-action");
}
void UnitEditor::clear_dirty() {
dirty_ = false;
gtk_widget_remove_css_class(btn_save_unit_, "suggested-action");
}
void UnitEditor::clear() {
g_signal_handlers_disconnect_by_data(entry_comment_, this);
current_task_ = nullptr;
current_unit_ = nullptr;
current_unit_name_.clear();
gtk_label_set_text(GTK_LABEL(name_display_), "");
gtk_editable_set_text(GTK_EDITABLE(entry_comment_), "");
gtk_widget_set_sensitive(task_section_, FALSE);
gtk_widget_set_sensitive(unit_controls_, FALSE);
clear_dirty();
}
void UnitEditor::load(Task* task, Unit* unit) {
gtk_widget_set_sensitive(task_section_, TRUE);
gtk_widget_set_sensitive(unit_controls_, TRUE);
g_signal_handlers_disconnect_by_data(entry_comment_, this);
current_task_ = task;
current_unit_ = unit;
current_unit_name_ = task ? task->name : "";
gtk_widget_set_sensitive(entry_comment_, TRUE);
if (unit) {
gtk_label_set_text(GTK_LABEL(name_display_), task->name.c_str());
} else {
auto markup = std::string("") + task->name + "";
gtk_label_set_markup(GTK_LABEL(name_display_), markup.c_str());
}
gtk_editable_set_text(GTK_EDITABLE(entry_comment_), task->comment.value_or("").c_str());
gtk_widget_set_sensitive(btn_edit_unit_, unit != nullptr);
// Connect comment change signal
g_signal_connect(entry_comment_, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) {
auto* self = static_cast(d);
if (!self->current_task_) return;
self->mark_dirty();
auto text = std::string(gtk_editable_get_text(e));
self->current_task_->comment = text.empty() ? std::nullopt : std::optional(text);
}), this);
clear_dirty();
}
void UnitEditor::save_current() {
if (!current_unit_) {
project_.report_status("Error: no unit loaded to save");
return;
}
auto* uf = project_.find_unit_file(current_unit_->name);
if (!uf) {
project_.report_status("Error: cannot find unit file for '" + current_unit_->name + "'");
return;
}
try {
uf->save();
clear_dirty();
project_.report_status("Saved unit file: " + uf->filepath.filename().string());
} catch (const std::exception& e) {
project_.report_status(std::string("Error: ") + e.what());
}
}
void UnitEditor::revert_current() {
if (current_unit_name_.empty()) return;
auto* uf = project_.find_unit_file(current_unit_name_);
if (uf) {
try {
auto reloaded = UnitFile::load(uf->filepath);
*uf = std::move(reloaded);
} catch (const std::exception& e) {
project_.report_status(std::string("Error: ") + e.what());
}
}
if (!project_.plans.empty()) {
auto& plan = project_.plans[0];
try {
auto reloaded = Plan::load(plan.filepath);
plan = std::move(reloaded);
} catch (const std::exception& e) {
project_.report_status(std::string("Error: ") + e.what());
}
}
clear_dirty();
}
void UnitEditor::set_unit_edited_callback(UnitEditedCallback cb, void* data) {
edit_cb_ = cb;
edit_cb_data_ = data;
}
void UnitEditor::on_edit_unit(GtkButton*, gpointer data) {
auto* self = static_cast(data);
if (!self->current_unit_) return;
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
auto result = show_unit_properties_dialog(parent, self->current_unit_,
self->project_, self->grex_config_, self->project_.all_shells());
if (result == UnitDialogResult::Save) {
auto new_name = self->current_unit_->name;
bool name_changed = (new_name != self->current_unit_name_);
self->current_unit_name_ = new_name;
if (name_changed) {
if (self->current_task_)
self->current_task_->name = new_name;
gtk_label_set_text(GTK_LABEL(self->name_display_), new_name.c_str());
}
self->mark_dirty();
if (self->edit_cb_)
self->edit_cb_(new_name, name_changed, self->edit_cb_data_);
}
}
void UnitEditor::on_select_unit(GtkButton*, gpointer data) {
auto* self = static_cast(data);
if (!self->current_task_) return;
if (self->project_.unit_files.empty())
self->project_.load_all_units();
if (self->project_.unit_files.empty()) {
self->project_.report_status("Error: no units loaded");
return;
}
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
show_unit_picker(parent, self->project_, [self](const std::string& unit_name) {
if (self->current_task_) self->current_task_->name = unit_name;
Unit* unit = self->project_.find_unit(unit_name);
self->load(self->current_task_, unit);
if (self->edit_cb_) self->edit_cb_(unit_name, true, self->edit_cb_data_);
});
}
}