Fix file selection alignment bug, add refresh buttons, polish UI

Replace fragile index-based file selection with direct pointer binding
via g_object_set_data on listbox rows in both UnitsView and ShellsView.
Add Refresh buttons to all three tabs (Units, Shells, Plans). Add status
bar notifications for shell and plan file loads. Wrap control panels in
labeled GtkFrame containers across all tabs. Improve config tab layout
with better typography, spacing, width constraint, and resolved path
status indicators.
This commit is contained in:
Chris Punches
2026-03-14 17:58:36 -04:00
parent 73960149fd
commit 85ad809887
7 changed files with 300 additions and 183 deletions

View File

@@ -166,19 +166,32 @@ void Project::load_plan(const fs::path& plan_path) {
void Project::reload_shells() {
auto sp = resolved_shells_path();
if (sp.empty()) return;
if (!fs::is_directory(sp)) return;
if (sp.empty()) {
report_status("Error: shells path not resolved");
return;
}
if (!fs::is_directory(sp)) {
report_status("Error: shells path is not a directory: " + sp.string());
return;
}
shell_files.clear();
int file_count = 0;
int total_shells = 0;
for (auto& entry : fs::directory_iterator(sp)) {
if (entry.path().extension() == ".shells") {
try {
shell_files.push_back(ShellsFile::load(entry.path()));
auto sf = ShellsFile::load(entry.path());
total_shells += (int)sf.shells.size();
file_count++;
shell_files.push_back(std::move(sf));
} catch (const std::exception& e) {
report_status("Error loading shells: " + std::string(e.what()));
}
}
}
report_status("Loaded " + std::to_string(total_shells) + " shells from " +
std::to_string(file_count) + " files at '" + sp.string() + "'");
}
std::vector<ShellDef> Project::all_shells() const {

View File

@@ -39,15 +39,23 @@ ConfigView::ConfigView(Project& project) : project_(project) {
root_ = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(root_), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_margin_start(box, 16);
gtk_widget_set_margin_end(box, 16);
gtk_widget_set_margin_top(box, 16);
gtk_widget_set_margin_bottom(box, 16);
// Outer centering wrapper — keeps form from stretching on wide screens
auto* outer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_vexpand(outer, TRUE);
// === Config file label + Open/Close buttons ===
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
gtk_widget_set_margin_start(box, 48);
gtk_widget_set_margin_end(box, 48);
gtk_widget_set_margin_top(box, 24);
gtk_widget_set_margin_bottom(box, 24);
gtk_widget_set_hexpand(box, TRUE);
gtk_widget_set_size_request(box, -1, -1);
// === Config file header ===
config_label_ = gtk_label_new(nullptr);
gtk_label_set_xalign(GTK_LABEL(config_label_), 0.0f);
gtk_label_set_selectable(GTK_LABEL(config_label_), TRUE);
gtk_widget_set_margin_bottom(config_label_, 4);
gtk_box_append(GTK_BOX(box), config_label_);
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
@@ -68,63 +76,61 @@ ConfigView::ConfigView(Project& project) : project_(project) {
g_signal_connect(btn_close_, "clicked", G_CALLBACK(on_close_config), this);
// === Config content — greyed out when no config loaded ===
config_content_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
// === Config fields section ===
auto* config_header = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(config_header), "<b>Configuration</b>");
gtk_label_set_xalign(GTK_LABEL(config_header), 0.0f);
gtk_box_append(GTK_BOX(config_content_), config_header);
config_content_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
// === Configuration fields ===
auto* config_frame = gtk_frame_new("Configuration");
config_grid_ = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(config_grid_), 8);
gtk_grid_set_column_spacing(GTK_GRID(config_grid_), 12);
gtk_box_append(GTK_BOX(config_content_), config_grid_);
gtk_grid_set_row_spacing(GTK_GRID(config_grid_), 10);
gtk_grid_set_column_spacing(GTK_GRID(config_grid_), 16);
gtk_widget_set_margin_start(config_grid_, 12);
gtk_widget_set_margin_end(config_grid_, 12);
gtk_widget_set_margin_top(config_grid_, 12);
gtk_widget_set_margin_bottom(config_grid_, 12);
gtk_frame_set_child(GTK_FRAME(config_frame), config_grid_);
gtk_box_append(GTK_BOX(config_content_), config_frame);
build_config_fields();
// === Resolved paths section ===
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
auto* resolved_header = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(resolved_header), "<b>Resolved Paths</b>");
gtk_label_set_xalign(GTK_LABEL(resolved_header), 0.0f);
gtk_box_append(GTK_BOX(config_content_), resolved_header);
// === Resolved Paths ===
auto* resolved_frame = gtk_frame_new("Resolved Paths");
resolved_grid_ = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(resolved_grid_), 4);
gtk_grid_set_column_spacing(GTK_GRID(resolved_grid_), 12);
gtk_box_append(GTK_BOX(config_content_), resolved_grid_);
gtk_grid_set_row_spacing(GTK_GRID(resolved_grid_), 6);
gtk_grid_set_column_spacing(GTK_GRID(resolved_grid_), 16);
gtk_widget_set_margin_start(resolved_grid_, 12);
gtk_widget_set_margin_end(resolved_grid_, 12);
gtk_widget_set_margin_top(resolved_grid_, 12);
gtk_widget_set_margin_bottom(resolved_grid_, 12);
gtk_frame_set_child(GTK_FRAME(resolved_frame), resolved_grid_);
gtk_box_append(GTK_BOX(config_content_), resolved_frame);
// === Variables section ===
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
auto* vars_header = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(vars_header), "<b>Variables</b>");
gtk_label_set_xalign(GTK_LABEL(vars_header), 0.0f);
gtk_box_append(GTK_BOX(config_content_), vars_header);
// === Variables ===
auto* vars_frame = gtk_frame_new("Variables");
auto* vars_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(vars_box, 12);
gtk_widget_set_margin_end(vars_box, 12);
gtk_widget_set_margin_top(vars_box, 12);
gtk_widget_set_margin_bottom(vars_box, 12);
auto* vars_desc = gtk_label_new("Set values for variables found in config fields. Environment variables are used automatically.");
gtk_label_set_xalign(GTK_LABEL(vars_desc), 0.0f);
gtk_label_set_wrap(GTK_LABEL(vars_desc), TRUE);
gtk_widget_add_css_class(vars_desc, "dim-label");
gtk_box_append(GTK_BOX(config_content_), vars_desc);
gtk_box_append(GTK_BOX(vars_box), vars_desc);
vars_grid_ = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(vars_grid_), 8);
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 12);
gtk_box_append(GTK_BOX(config_content_), vars_grid_);
gtk_grid_set_row_spacing(GTK_GRID(vars_grid_), 10);
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 16);
gtk_box_append(GTK_BOX(vars_box), vars_grid_);
gtk_frame_set_child(GTK_FRAME(vars_frame), vars_box);
gtk_box_append(GTK_BOX(config_content_), vars_frame);
build_variables_section();
update_resolved_labels();
// === Save button ===
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
// === Save button — full width for presence ===
btn_save_ = gtk_button_new_with_label("Save Config");
gtk_widget_set_halign(btn_save_, GTK_ALIGN_END);
gtk_box_append(GTK_BOX(config_content_), btn_save_);
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
@@ -132,8 +138,9 @@ ConfigView::ConfigView(Project& project) : project_(project) {
}), this);
gtk_box_append(GTK_BOX(box), config_content_);
gtk_box_append(GTK_BOX(outer), box);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), box);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), outer);
update_config_buttons();
}
@@ -153,6 +160,8 @@ void ConfigView::build_config_fields() {
for (auto& [key, val] : project_.config.data().items()) {
auto* label = gtk_label_new(key.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 1.0f);
gtk_widget_set_size_request(label, 140, -1);
gtk_widget_add_css_class(label, "dim-label");
gtk_grid_attach(GTK_GRID(config_grid_), label, 0, row, 1, 1);
auto* entry = gtk_entry_new();
@@ -209,6 +218,8 @@ void ConfigView::build_variables_section() {
auto var_label = "${" + name + "}";
auto* lbl = gtk_label_new(var_label.c_str());
gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f);
gtk_widget_set_size_request(lbl, 140, -1);
gtk_widget_add_css_class(lbl, "dim-label");
gtk_grid_attach(GTK_GRID(vars_grid_), lbl, 0, row, 1, 1);
auto* entry = gtk_entry_new();
@@ -277,9 +288,25 @@ void ConfigView::update_resolved_labels() {
display = resolved;
}
auto* lbl = gtk_label_new(key.c_str());
gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f);
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, row, 1, 1);
auto* key_lbl = gtk_label_new(key.c_str());
gtk_label_set_xalign(GTK_LABEL(key_lbl), 1.0f);
gtk_widget_set_size_request(key_lbl, 140, -1);
gtk_widget_add_css_class(key_lbl, "dim-label");
gtk_grid_attach(GTK_GRID(resolved_grid_), key_lbl, 0, row, 1, 1);
// Status indicator
bool path_ok = (display != "(unresolved)") &&
(std::filesystem::exists(display) || std::filesystem::is_directory(display));
auto* indicator = gtk_label_new(nullptr);
gtk_label_set_xalign(GTK_LABEL(indicator), 0.5f);
if (display == "(unresolved)")
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#cc0000\">\u2718</span>");
else if (path_ok)
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#4e9a06\">\u2714</span>");
else
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#cc0000\">\u2718</span>");
gtk_grid_attach(GTK_GRID(resolved_grid_), indicator, 1, row, 1, 1);
auto* val_label = gtk_label_new(nullptr);
gtk_label_set_xalign(GTK_LABEL(val_label), 0.0f);
gtk_label_set_selectable(GTK_LABEL(val_label), TRUE);
@@ -287,26 +314,22 @@ void ConfigView::update_resolved_labels() {
if (display == "(unresolved)") {
gtk_label_set_markup(GTK_LABEL(val_label),
"<span foreground=\"red\">(unresolved)</span>");
} else if (std::filesystem::is_directory(display)) {
auto markup = "<span foreground=\"green\">" + display + "</span>";
gtk_label_set_markup(GTK_LABEL(val_label), markup.c_str());
} else if (std::filesystem::exists(display)) {
auto markup = "<span foreground=\"green\">" + display + "</span>";
gtk_label_set_markup(GTK_LABEL(val_label), markup.c_str());
"<span foreground=\"#cc0000\" style=\"italic\">(unresolved)</span>");
} else if (path_ok) {
gtk_label_set_text(GTK_LABEL(val_label), display.c_str());
} else {
auto markup = "<span foreground=\"red\">" + display + "</span>";
auto markup = "<span foreground=\"#cc0000\">" + display + "</span>";
gtk_label_set_markup(GTK_LABEL(val_label), markup.c_str());
}
gtk_grid_attach(GTK_GRID(resolved_grid_), val_label, 1, row, 1, 1);
gtk_grid_attach(GTK_GRID(resolved_grid_), val_label, 2, row, 1, 1);
row++;
}
if (row == 0) {
auto* lbl = gtk_label_new("No resolvable paths in config.");
gtk_label_set_xalign(GTK_LABEL(lbl), 0.0f);
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, 0, 2, 1);
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, 0, 3, 1);
}
}
@@ -338,10 +361,15 @@ void ConfigView::apply_config() {
void ConfigView::update_config_buttons() {
bool has_config = !project_.config_path.empty();
if (has_config) {
auto markup = std::string("<b>Current Rex Config:</b> ") + project_.config_path.filename().string();
auto markup = std::string("<span size=\"large\" weight=\"bold\">Rex Config: ") +
project_.config_path.filename().string() + "</span>\n" +
"<span size=\"small\" alpha=\"60%\">" +
project_.config_path.string() + "</span>";
gtk_label_set_markup(GTK_LABEL(config_label_), markup.c_str());
} else {
gtk_label_set_markup(GTK_LABEL(config_label_), "<b>Current Rex Config:</b> No config loaded");
gtk_label_set_markup(GTK_LABEL(config_label_),
"<span size=\"large\" weight=\"bold\">Rex Config</span>\n"
"<span size=\"small\" alpha=\"60%\">No config loaded</span>");
}
gtk_widget_set_visible(btn_open_, !has_config);
gtk_widget_set_visible(btn_create_, !has_config);

View File

@@ -74,7 +74,12 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
gtk_box_append(GTK_BOX(task_controls_), scroll);
// Task action buttons — grouped by function
auto* plan_ctrl_frame = gtk_frame_new("Plan Controls");
auto* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(btn_box, 8);
gtk_widget_set_margin_end(btn_box, 8);
gtk_widget_set_margin_top(btn_box, 8);
gtk_widget_set_margin_bottom(btn_box, 8);
auto* task_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(task_edit_group, "linked");
@@ -86,8 +91,8 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(move_group, "linked");
auto* btn_up = gtk_button_new_with_label("Up");
auto* btn_down = gtk_button_new_with_label("Down");
auto* btn_up = gtk_button_new_with_label("Move Up");
auto* btn_down = gtk_button_new_with_label("Move Down");
gtk_box_append(GTK_BOX(move_group), btn_up);
gtk_box_append(GTK_BOX(move_group), btn_down);
gtk_box_append(GTK_BOX(btn_box), move_group);
@@ -95,7 +100,11 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
btn_save_plan_ = gtk_button_new_with_label("Save Plan");
gtk_box_append(GTK_BOX(btn_box), btn_save_plan_);
gtk_box_append(GTK_BOX(task_controls_), btn_box);
auto* btn_refresh = gtk_button_new_with_label("Refresh");
gtk_box_append(GTK_BOX(btn_box), btn_refresh);
gtk_frame_set_child(GTK_FRAME(plan_ctrl_frame), btn_box);
gtk_box_append(GTK_BOX(task_controls_), plan_ctrl_frame);
gtk_box_append(GTK_BOX(left), task_controls_);
gtk_paned_set_start_child(GTK_PANED(root_), left);
@@ -136,6 +145,11 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
}
}), this);
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<PlanView*>(d);
self->refresh();
}), this);
update_plan_buttons();
}
@@ -461,11 +475,19 @@ void PlanView::refresh() {
// reload units if paths now resolve
project_.load_all_units();
// reload the plan from disk if one is loaded
auto* plan = current_plan();
if (plan)
if (plan) {
try {
*plan = Plan::load(plan->filepath);
project_.report_status("Reloaded plan: " + plan->filepath.filename().string());
} catch (const std::exception& e) {
project_.report_status("Error reloading plan: " + std::string(e.what()));
}
gtk_label_set_markup(GTK_LABEL(plan_label_), (std::string("<b>Plan:</b> ") + plan->filepath.filename().string()).c_str());
else
} else {
gtk_label_set_markup(GTK_LABEL(plan_label_), "<b>Plan:</b> No plan loaded");
}
populate_task_list();
update_plan_buttons();

View File

@@ -17,14 +17,9 @@
*/
#include "views/shells_view.h"
#include <algorithm>
#include <cstring>
static int sort_listbox_alpha(GtkListBoxRow* a, GtkListBoxRow* b, gpointer) {
auto* la = GTK_LABEL(gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(a)));
auto* lb = GTK_LABEL(gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(b)));
return std::strcmp(gtk_label_get_text(la), gtk_label_get_text(lb));
}
namespace grex {
ShellsView::ShellsView(Project& project) : project_(project) {
@@ -50,11 +45,15 @@ ShellsView::ShellsView(Project& project) : project_(project) {
gtk_widget_add_css_class(file_scroll, "frame");
file_listbox_ = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(file_listbox_), GTK_SELECTION_SINGLE);
gtk_list_box_set_sort_func(GTK_LIST_BOX(file_listbox_), sort_listbox_alpha, nullptr, nullptr);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(file_scroll), file_listbox_);
gtk_box_append(GTK_BOX(left), file_scroll);
auto* file_ctrl_frame = gtk_frame_new("File Controls");
auto* file_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(file_btn_box, 8);
gtk_widget_set_margin_end(file_btn_box, 8);
gtk_widget_set_margin_top(file_btn_box, 8);
gtk_widget_set_margin_bottom(file_btn_box, 8);
auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(file_edit_group, "linked");
@@ -67,7 +66,11 @@ ShellsView::ShellsView(Project& project) : project_(project) {
btn_save_ = gtk_button_new_with_label("Save File");
gtk_box_append(GTK_BOX(file_btn_box), btn_save_);
gtk_box_append(GTK_BOX(left), file_btn_box);
auto* btn_refresh = gtk_button_new_with_label("Refresh");
gtk_box_append(GTK_BOX(file_btn_box), btn_refresh);
gtk_frame_set_child(GTK_FRAME(file_ctrl_frame), file_btn_box);
gtk_box_append(GTK_BOX(left), file_ctrl_frame);
gtk_paned_set_start_child(GTK_PANED(root_), left);
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE);
@@ -94,7 +97,12 @@ ShellsView::ShellsView(Project& project) : project_(project) {
gtk_box_append(GTK_BOX(right), shell_scroll);
// Shell action buttons
auto* shell_ctrl_frame = gtk_frame_new("Shell Controls");
auto* shell_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(shell_btn_box, 8);
gtk_widget_set_margin_end(shell_btn_box, 8);
gtk_widget_set_margin_top(shell_btn_box, 8);
gtk_widget_set_margin_bottom(shell_btn_box, 8);
auto* shell_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(shell_edit_group, "linked");
@@ -106,13 +114,14 @@ ShellsView::ShellsView(Project& project) : project_(project) {
auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(move_group, "linked");
auto* btn_move_up = gtk_button_new_with_label("Up");
auto* btn_move_down = gtk_button_new_with_label("Down");
auto* btn_move_up = gtk_button_new_with_label("Move Up");
auto* btn_move_down = gtk_button_new_with_label("Move Down");
gtk_box_append(GTK_BOX(move_group), btn_move_up);
gtk_box_append(GTK_BOX(move_group), btn_move_down);
gtk_box_append(GTK_BOX(shell_btn_box), move_group);
gtk_box_append(GTK_BOX(right), shell_btn_box);
gtk_frame_set_child(GTK_FRAME(shell_ctrl_frame), shell_btn_box);
gtk_box_append(GTK_BOX(right), shell_ctrl_frame);
// Shell properties editor
gtk_box_append(GTK_BOX(right), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
@@ -155,6 +164,11 @@ ShellsView::ShellsView(Project& project) : project_(project) {
g_signal_connect(btn_move_up, "clicked", G_CALLBACK(on_move_up), this);
g_signal_connect(btn_move_down, "clicked", G_CALLBACK(on_move_down), this);
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<ShellsView*>(d);
self->refresh();
}), this);
g_signal_connect(shell_listbox_, "row-selected", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer d) {
auto* self = static_cast<ShellsView*>(d);
if (!row) { self->clear_editor(); return; }
@@ -163,11 +177,11 @@ ShellsView::ShellsView(Project& project) : project_(project) {
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<ShellsView*>(d);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size()) {
if (!self->selected_file_) {
self->project_.report_status("Error: no shell file selected");
return;
}
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto& sf = *self->selected_file_;
try {
sf.save();
self->file_dirty_ = false;
@@ -182,9 +196,9 @@ ShellsView::ShellsView(Project& project) : project_(project) {
auto bind = [this](GtkWidget* entry) {
g_signal_connect(entry, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) {
auto* self = static_cast<ShellsView*>(d);
if (self->loading_ || self->current_file_idx_ < 0 || self->current_shell_idx_ < 0)
if (self->loading_ || !self->selected_file_ || self->current_shell_idx_ < 0)
return;
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto& sf = *self->selected_file_;
if (self->current_shell_idx_ >= (int)sf.shells.size()) return;
auto& s = sf.shells[self->current_shell_idx_];
auto text = std::string(gtk_editable_get_text(e));
@@ -215,15 +229,29 @@ void ShellsView::mark_file_dirty() {
gtk_widget_add_css_class(btn_save_, "suggested-action");
}
GtkListBoxRow* ShellsView::find_file_row(ShellsFile* sf) {
for (int i = 0; ; i++) {
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(file_listbox_), i);
if (!row) return nullptr;
if (g_object_get_data(G_OBJECT(row), "shell-file") == sf) return row;
}
}
void ShellsView::populate_file_list() {
GtkWidget* child;
while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr)
gtk_list_box_remove(GTK_LIST_BOX(file_listbox_), child);
current_file_idx_ = -1;
selected_file_ = nullptr;
std::sort(project_.shell_files.begin(), project_.shell_files.end(),
[](const ShellsFile& a, const ShellsFile& b) {
return a.filepath.filename().string() < b.filepath.filename().string();
});
for (auto& sf : project_.shell_files) {
auto* row = gtk_list_box_row_new();
g_object_set_data(G_OBJECT(row), "shell-file", &sf);
auto text = std::string("\u25C6 ") + sf.filepath.filename().string();
auto* label = gtk_label_new(text.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
@@ -246,16 +274,15 @@ void ShellsView::populate_shell_list() {
current_shell_idx_ = -1;
clear_editor();
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) {
if (!selected_file_) {
gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
return;
}
auto& sf = project_.shell_files[current_file_idx_];
gtk_label_set_markup(GTK_LABEL(file_label_),
(std::string("<b>") + sf.filepath.filename().string() + "</b>").c_str());
(std::string("<b>") + selected_file_->filepath.filename().string() + "</b>").c_str());
for (auto& s : sf.shells) {
for (auto& s : selected_file_->shells) {
auto* row = gtk_list_box_row_new();
auto text = std::string("\u25B8 ") + s.name;
auto* label = gtk_label_new(text.c_str());
@@ -280,11 +307,11 @@ void ShellsView::clear_editor() {
}
void ShellsView::load_shell(int idx) {
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) {
if (!selected_file_) {
clear_editor();
return;
}
auto& sf = project_.shell_files[current_file_idx_];
auto& sf = *selected_file_;
if (idx < 0 || idx >= (int)sf.shells.size()) {
clear_editor();
return;
@@ -308,11 +335,11 @@ void ShellsView::refresh() {
void ShellsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data) {
auto* self = static_cast<ShellsView*>(data);
if (!row) {
self->current_file_idx_ = -1;
self->selected_file_ = nullptr;
self->populate_shell_list();
return;
}
self->current_file_idx_ = gtk_list_box_row_get_index(row);
self->selected_file_ = static_cast<ShellsFile*>(g_object_get_data(G_OBJECT(row), "shell-file"));
self->file_dirty_ = false;
gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
self->populate_shell_list();
@@ -371,10 +398,14 @@ void ShellsView::on_new_file_response(GObject* source, GAsyncResult* res, gpoint
self->project_.shell_files.push_back(std::move(sf));
self->populate_file_list();
int last = (int)self->project_.shell_files.size() - 1;
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), last);
if (row)
gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
// Select the new file by matching filepath
for (auto& f : self->project_.shell_files) {
if (f.filepath == fp) {
auto* row = self->find_file_row(&f);
if (row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
break;
}
}
self->project_.report_status("Created shell file: " + fp.filename().string());
} catch (const std::exception& e) {
@@ -384,26 +415,28 @@ void ShellsView::on_new_file_response(GObject* source, GAsyncResult* res, gpoint
void ShellsView::on_delete_file(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
return;
if (!self->selected_file_) return;
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto name = sf.filepath.filename().string();
auto name = self->selected_file_->filepath.filename().string();
self->project_.shell_files.erase(self->project_.shell_files.begin() + self->current_file_idx_);
self->current_file_idx_ = -1;
auto it = std::find_if(self->project_.shell_files.begin(), self->project_.shell_files.end(),
[&](const ShellsFile& sf) { return &sf == self->selected_file_; });
if (it != self->project_.shell_files.end())
self->project_.shell_files.erase(it);
self->selected_file_ = nullptr;
self->populate_file_list();
self->project_.report_status("Removed shell file from project: " + name + " (file not deleted from disk)");
}
void ShellsView::on_new_shell(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size()) {
if (!self->selected_file_) {
self->project_.report_status("Error: select a shell file first");
return;
}
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto& sf = *self->selected_file_;
ShellDef s;
s.name = "new_shell";
s.path = "/usr/bin/new_shell";
@@ -421,14 +454,13 @@ void ShellsView::on_new_shell(GtkButton*, gpointer data) {
void ShellsView::on_delete_shell(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
if (!row) return;
int idx = gtk_list_box_row_get_index(row);
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto& sf = *self->selected_file_;
if (idx < 0 || idx >= (int)sf.shells.size()) return;
auto name = sf.shells[idx].name;
@@ -440,14 +472,13 @@ void ShellsView::on_delete_shell(GtkButton*, gpointer data) {
void ShellsView::on_move_up(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
if (!row) return;
int idx = gtk_list_box_row_get_index(row);
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto& sf = *self->selected_file_;
if (idx <= 0) return;
std::swap(sf.shells[idx], sf.shells[idx - 1]);
@@ -460,14 +491,13 @@ void ShellsView::on_move_up(GtkButton*, gpointer data) {
void ShellsView::on_move_down(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
if (!row) return;
int idx = gtk_list_box_row_get_index(row);
auto& sf = self->project_.shell_files[self->current_file_idx_];
auto& sf = *self->selected_file_;
if (idx < 0 || idx >= (int)sf.shells.size() - 1) return;
std::swap(sf.shells[idx], sf.shells[idx + 1]);

View File

@@ -47,7 +47,7 @@ private:
GtkWidget* entry_exec_arg_;
GtkWidget* entry_source_cmd_;
int current_file_idx_ = -1;
ShellsFile* selected_file_ = nullptr;
int current_shell_idx_ = -1;
bool loading_ = false;
bool file_dirty_ = false;
@@ -57,6 +57,7 @@ private:
void load_shell(int idx);
void clear_editor();
void mark_file_dirty();
GtkListBoxRow* find_file_row(ShellsFile* sf);
static void on_file_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);
static void on_new_file(GtkButton* btn, gpointer data);

View File

@@ -19,15 +19,10 @@
#include "views/units_view.h"
#include "util/unsaved_dialog.h"
#include "util/unit_properties_dialog.h"
#include <algorithm>
#include <cstring>
#include <fstream>
static int sort_listbox_alpha(GtkListBoxRow* a, GtkListBoxRow* b, gpointer) {
auto* la = GTK_LABEL(gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(a)));
auto* lb = GTK_LABEL(gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(b)));
return std::strcmp(gtk_label_get_text(la), gtk_label_get_text(lb));
}
namespace grex {
UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
@@ -54,12 +49,16 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
gtk_widget_add_css_class(file_scroll, "frame");
file_listbox_ = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(file_listbox_), GTK_SELECTION_SINGLE);
gtk_list_box_set_sort_func(GTK_LIST_BOX(file_listbox_), sort_listbox_alpha, nullptr, nullptr);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(file_scroll), file_listbox_);
gtk_box_append(GTK_BOX(left), file_scroll);
// File lifecycle buttons — grouped
auto* file_ctrl_frame = gtk_frame_new("File Controls");
auto* file_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(file_btn_box, 8);
gtk_widget_set_margin_end(file_btn_box, 8);
gtk_widget_set_margin_top(file_btn_box, 8);
gtk_widget_set_margin_bottom(file_btn_box, 8);
auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(file_edit_group, "linked");
@@ -72,7 +71,11 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
btn_save_ = gtk_button_new_with_label("Save File");
gtk_box_append(GTK_BOX(file_btn_box), btn_save_);
gtk_box_append(GTK_BOX(left), file_btn_box);
auto* btn_refresh = gtk_button_new_with_label("Refresh");
gtk_box_append(GTK_BOX(file_btn_box), btn_refresh);
gtk_frame_set_child(GTK_FRAME(file_ctrl_frame), file_btn_box);
gtk_box_append(GTK_BOX(left), file_ctrl_frame);
gtk_paned_set_start_child(GTK_PANED(root_), left);
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE);
@@ -100,7 +103,12 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
gtk_box_append(GTK_BOX(right), unit_scroll);
// Unit action buttons — grouped by function
auto* unit_ctrl_frame = gtk_frame_new("Unit Controls");
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);
auto* unit_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(unit_edit_group, "linked");
@@ -115,13 +123,14 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(move_group, "linked");
auto* btn_move_up = gtk_button_new_with_label("Up");
auto* btn_move_down = gtk_button_new_with_label("Down");
auto* btn_move_up = gtk_button_new_with_label("Move Up");
auto* btn_move_down = gtk_button_new_with_label("Move Down");
gtk_box_append(GTK_BOX(move_group), btn_move_up);
gtk_box_append(GTK_BOX(move_group), btn_move_down);
gtk_box_append(GTK_BOX(unit_btn_box), move_group);
gtk_box_append(GTK_BOX(right), unit_btn_box);
gtk_frame_set_child(GTK_FRAME(unit_ctrl_frame), unit_btn_box);
gtk_box_append(GTK_BOX(right), unit_ctrl_frame);
gtk_paned_set_end_child(GTK_PANED(root_), right);
gtk_paned_set_shrink_end_child(GTK_PANED(root_), FALSE);
@@ -137,13 +146,18 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
g_signal_connect(btn_move_down, "clicked", G_CALLBACK(on_move_down), this);
g_signal_connect(unit_listbox_, "row-activated", G_CALLBACK(on_unit_activated), this);
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<UnitsView*>(d);
self->refresh();
}), this);
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<UnitsView*>(d);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size()) {
if (!self->selected_file_) {
self->project_.report_status("Error: no unit file selected");
return;
}
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto& uf = *self->selected_file_;
// Check for cross-file duplicates before saving
for (auto& u : uf.units) {
if (self->project_.is_unit_name_taken(u.name, &u)) {
@@ -162,15 +176,29 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
}), this);
}
GtkListBoxRow* UnitsView::find_file_row(UnitFile* uf) {
for (int i = 0; ; i++) {
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(file_listbox_), i);
if (!row) return nullptr;
if (g_object_get_data(G_OBJECT(row), "unit-file") == uf) return row;
}
}
void UnitsView::populate_file_list() {
GtkWidget* child;
while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr)
gtk_list_box_remove(GTK_LIST_BOX(file_listbox_), child);
current_file_idx_ = -1;
selected_file_ = nullptr;
std::sort(project_.unit_files.begin(), project_.unit_files.end(),
[](const UnitFile& a, const UnitFile& b) {
return a.filepath.filename().string() < b.filepath.filename().string();
});
for (auto& uf : project_.unit_files) {
auto* row = gtk_list_box_row_new();
g_object_set_data(G_OBJECT(row), "unit-file", &uf);
auto text = std::string("\u25C6 ") + uf.filepath.filename().string();
auto* label = gtk_label_new(text.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
@@ -190,16 +218,15 @@ void UnitsView::populate_unit_list() {
while ((child = gtk_widget_get_first_child(unit_listbox_)) != nullptr)
gtk_list_box_remove(GTK_LIST_BOX(unit_listbox_), child);
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.unit_files.size()) {
if (!selected_file_) {
gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
return;
}
auto& uf = project_.unit_files[current_file_idx_];
gtk_label_set_markup(GTK_LABEL(file_label_),
(std::string("<b>") + uf.filepath.filename().string() + "</b>").c_str());
(std::string("<b>") + selected_file_->filepath.filename().string() + "</b>").c_str());
for (auto& u : uf.units) {
for (auto& u : selected_file_->units) {
auto* row = gtk_list_box_row_new();
auto text = std::string("\u2022 ") + u.name;
auto* label = gtk_label_new(text.c_str());
@@ -219,14 +246,12 @@ void UnitsView::refresh() {
}
void UnitsView::save_current_file() {
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.unit_files.size())
return;
auto& uf = project_.unit_files[current_file_idx_];
if (!selected_file_) return;
try {
uf.save();
selected_file_->save();
file_dirty_ = false;
gtk_widget_remove_css_class(btn_save_, "suggested-action");
project_.report_status("Saved unit file: " + uf.filepath.filename().string());
project_.report_status("Saved unit file: " + selected_file_->filepath.filename().string());
} catch (const std::exception& e) {
project_.report_status(std::string("Error: ") + e.what());
}
@@ -237,17 +262,17 @@ void UnitsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data)
if (self->suppress_selection_) return;
if (!row) {
self->current_file_idx_ = -1;
self->selected_file_ = nullptr;
self->populate_unit_list();
return;
}
int new_idx = gtk_list_box_row_get_index(row);
auto* new_file = static_cast<UnitFile*>(g_object_get_data(G_OBJECT(row), "unit-file"));
if (self->file_dirty_ && self->current_file_idx_ >= 0 && new_idx != self->current_file_idx_) {
int old_idx = self->current_file_idx_;
if (self->file_dirty_ && self->selected_file_ && new_file != self->selected_file_) {
// Snap selection back to the old file while showing the dialog
self->suppress_selection_ = true;
auto* old_row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), old_idx);
auto* old_row = self->find_file_row(self->selected_file_);
if (old_row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), old_row);
self->suppress_selection_ = false;
@@ -256,31 +281,28 @@ void UnitsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data)
if (result == UnsavedResult::Cancel)
return;
if (result == UnsavedResult::Save) {
auto& uf = self->project_.unit_files[self->current_file_idx_];
try { uf.save(); } catch (const std::exception& e) {
try { self->selected_file_->save(); } catch (const std::exception& e) {
self->project_.report_status(std::string("Error: ") + e.what());
}
} else {
auto idx = self->current_file_idx_;
if (idx >= 0 && idx < (int)self->project_.unit_files.size()) {
auto& uf = self->project_.unit_files[idx];
try { uf = UnitFile::load(uf.filepath); } catch (const std::exception& e) {
self->project_.report_status(std::string("Error: ") + e.what());
}
try {
*self->selected_file_ = UnitFile::load(self->selected_file_->filepath);
} catch (const std::exception& e) {
self->project_.report_status(std::string("Error: ") + e.what());
}
}
self->file_dirty_ = false; gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
// Now snap to the new selection
self->suppress_selection_ = true;
auto* target_row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), new_idx);
if (target_row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), target_row);
gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
self->suppress_selection_ = false;
self->current_file_idx_ = new_idx;
self->selected_file_ = new_file;
self->populate_unit_list();
return;
}
self->current_file_idx_ = new_idx;
self->selected_file_ = new_file;
self->file_dirty_ = false; gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
self->populate_unit_list();
}
@@ -340,11 +362,14 @@ void UnitsView::on_new_file_response(GObject* source, GAsyncResult* res, gpointe
self->project_.unit_files.push_back(std::move(uf));
self->populate_file_list();
// Select the new file
int last = (int)self->project_.unit_files.size() - 1;
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), last);
if (row)
gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
// Select the new file by matching filepath
for (auto& f : self->project_.unit_files) {
if (f.filepath == fp) {
auto* row = self->find_file_row(&f);
if (row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
break;
}
}
self->project_.report_status("Created unit file: " + fp.filename().string());
} catch (const std::exception& e) {
@@ -354,21 +379,23 @@ void UnitsView::on_new_file_response(GObject* source, GAsyncResult* res, gpointe
void UnitsView::on_delete_file(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size())
return;
if (!self->selected_file_) return;
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto name = uf.filepath.filename().string();
auto name = self->selected_file_->filepath.filename().string();
self->project_.unit_files.erase(self->project_.unit_files.begin() + self->current_file_idx_);
self->current_file_idx_ = -1;
auto it = std::find_if(self->project_.unit_files.begin(), self->project_.unit_files.end(),
[&](const UnitFile& uf) { return &uf == self->selected_file_; });
if (it != self->project_.unit_files.end())
self->project_.unit_files.erase(it);
self->selected_file_ = nullptr;
self->populate_file_list();
self->project_.report_status("Removed unit file from project: " + name + " (file not deleted from disk)");
}
void UnitsView::on_new_unit(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size()) {
if (!self->selected_file_) {
self->project_.report_status("Error: select a unit file first");
return;
}
@@ -435,7 +462,7 @@ void UnitsView::on_new_unit(GtkButton*, gpointer data) {
return;
}
auto& uf = nd->view->project_.unit_files[nd->view->current_file_idx_];
auto& uf = *nd->view->selected_file_;
Unit u;
u.name = name;
@@ -453,14 +480,13 @@ void UnitsView::on_new_unit(GtkButton*, gpointer data) {
void UnitsView::on_delete_unit(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) return;
int idx = gtk_list_box_row_get_index(row);
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto& uf = *self->selected_file_;
if (idx < 0 || idx >= (int)uf.units.size()) return;
auto name = uf.units[idx].name;
@@ -472,10 +498,9 @@ void UnitsView::on_delete_unit(GtkButton*, gpointer data) {
void UnitsView::on_unit_activated(GtkListBox*, GtkListBoxRow* row, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size())
return;
if (!self->selected_file_) return;
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto& uf = *self->selected_file_;
// Get unit name from the row's label (strip bullet prefix)
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
@@ -503,13 +528,12 @@ void UnitsView::on_unit_activated(GtkListBox*, GtkListBoxRow* row, gpointer data
void UnitsView::on_move_up(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) return;
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto& uf = *self->selected_file_;
// Find unit index by name from the row label
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
@@ -535,13 +559,12 @@ void UnitsView::on_move_up(GtkButton*, gpointer data) {
void UnitsView::on_move_down(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) return;
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto& uf = *self->selected_file_;
// Find unit index by name from the row label
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
@@ -567,8 +590,7 @@ void UnitsView::on_move_down(GtkButton*, gpointer data) {
void UnitsView::on_edit_unit(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data);
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.unit_files.size())
return;
if (!self->selected_file_) return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) {
@@ -583,7 +605,7 @@ void UnitsView::on_edit_unit(GtkButton*, gpointer data) {
label_text = label_text.substr(strlen("\u2022 "));
// Find unit by name
auto& uf = self->project_.unit_files[self->current_file_idx_];
auto& uf = *self->selected_file_;
Unit* unit = nullptr;
for (auto& u : uf.units) {
if (u.name == label_text) { unit = &u; break; }

View File

@@ -40,12 +40,13 @@ private:
GtkWidget* file_label_;
GtkWidget* btn_save_;
int current_file_idx_ = -1;
UnitFile* selected_file_ = nullptr;
bool file_dirty_ = false;
bool suppress_selection_ = false;
void populate_file_list();
void populate_unit_list();
GtkListBoxRow* find_file_row(UnitFile* uf);
static void on_file_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);
static void on_new_file(GtkButton* btn, gpointer data);