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() { void Project::reload_shells() {
auto sp = resolved_shells_path(); auto sp = resolved_shells_path();
if (sp.empty()) return; if (sp.empty()) {
if (!fs::is_directory(sp)) return; 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(); shell_files.clear();
int file_count = 0;
int total_shells = 0;
for (auto& entry : fs::directory_iterator(sp)) { for (auto& entry : fs::directory_iterator(sp)) {
if (entry.path().extension() == ".shells") { if (entry.path().extension() == ".shells") {
try { 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) { } catch (const std::exception& e) {
report_status("Error loading shells: " + std::string(e.what())); 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 { std::vector<ShellDef> Project::all_shells() const {

View File

@@ -39,15 +39,23 @@ ConfigView::ConfigView(Project& project) : project_(project) {
root_ = gtk_scrolled_window_new(); root_ = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(root_), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(root_), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); // Outer centering wrapper — keeps form from stretching on wide screens
gtk_widget_set_margin_start(box, 16); auto* outer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_set_margin_end(box, 16); gtk_widget_set_vexpand(outer, TRUE);
gtk_widget_set_margin_top(box, 16);
gtk_widget_set_margin_bottom(box, 16);
// === 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); config_label_ = gtk_label_new(nullptr);
gtk_label_set_xalign(GTK_LABEL(config_label_), 0.0f); 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_); gtk_box_append(GTK_BOX(box), config_label_);
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); 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); g_signal_connect(btn_close_, "clicked", G_CALLBACK(on_close_config), this);
// === Config content — greyed out when no config loaded === // === Config content — greyed out when no config loaded ===
config_content_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12); config_content_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
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);
// === Configuration fields ===
auto* config_frame = gtk_frame_new("Configuration");
config_grid_ = gtk_grid_new(); config_grid_ = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(config_grid_), 8); gtk_grid_set_row_spacing(GTK_GRID(config_grid_), 10);
gtk_grid_set_column_spacing(GTK_GRID(config_grid_), 12); gtk_grid_set_column_spacing(GTK_GRID(config_grid_), 16);
gtk_box_append(GTK_BOX(config_content_), config_grid_); 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(); build_config_fields();
// === Resolved paths section === // === Resolved Paths ===
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); auto* resolved_frame = gtk_frame_new("Resolved Paths");
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_grid_ = gtk_grid_new(); resolved_grid_ = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(resolved_grid_), 4); gtk_grid_set_row_spacing(GTK_GRID(resolved_grid_), 6);
gtk_grid_set_column_spacing(GTK_GRID(resolved_grid_), 12); gtk_grid_set_column_spacing(GTK_GRID(resolved_grid_), 16);
gtk_box_append(GTK_BOX(config_content_), resolved_grid_); 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 === // === Variables ===
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); auto* vars_frame = gtk_frame_new("Variables");
auto* vars_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
auto* vars_header = gtk_label_new(nullptr); gtk_widget_set_margin_start(vars_box, 12);
gtk_label_set_markup(GTK_LABEL(vars_header), "<b>Variables</b>"); gtk_widget_set_margin_end(vars_box, 12);
gtk_label_set_xalign(GTK_LABEL(vars_header), 0.0f); gtk_widget_set_margin_top(vars_box, 12);
gtk_box_append(GTK_BOX(config_content_), vars_header); 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."); 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_xalign(GTK_LABEL(vars_desc), 0.0f);
gtk_label_set_wrap(GTK_LABEL(vars_desc), TRUE); gtk_label_set_wrap(GTK_LABEL(vars_desc), TRUE);
gtk_widget_add_css_class(vars_desc, "dim-label"); 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(); vars_grid_ = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(vars_grid_), 8); gtk_grid_set_row_spacing(GTK_GRID(vars_grid_), 10);
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 12); gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 16);
gtk_box_append(GTK_BOX(config_content_), vars_grid_); 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(); build_variables_section();
update_resolved_labels(); update_resolved_labels();
// === Save button === // === Save button — full width for presence ===
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
btn_save_ = gtk_button_new_with_label("Save Config"); 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_); gtk_box_append(GTK_BOX(config_content_), btn_save_);
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) { g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
@@ -132,8 +138,9 @@ ConfigView::ConfigView(Project& project) : project_(project) {
}), this); }), this);
gtk_box_append(GTK_BOX(box), config_content_); 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(); update_config_buttons();
} }
@@ -153,6 +160,8 @@ void ConfigView::build_config_fields() {
for (auto& [key, val] : project_.config.data().items()) { for (auto& [key, val] : project_.config.data().items()) {
auto* label = gtk_label_new(key.c_str()); auto* label = gtk_label_new(key.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 1.0f); 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); gtk_grid_attach(GTK_GRID(config_grid_), label, 0, row, 1, 1);
auto* entry = gtk_entry_new(); auto* entry = gtk_entry_new();
@@ -209,6 +218,8 @@ void ConfigView::build_variables_section() {
auto var_label = "${" + name + "}"; auto var_label = "${" + name + "}";
auto* lbl = gtk_label_new(var_label.c_str()); auto* lbl = gtk_label_new(var_label.c_str());
gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f); 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); gtk_grid_attach(GTK_GRID(vars_grid_), lbl, 0, row, 1, 1);
auto* entry = gtk_entry_new(); auto* entry = gtk_entry_new();
@@ -277,9 +288,25 @@ void ConfigView::update_resolved_labels() {
display = resolved; display = resolved;
} }
auto* lbl = gtk_label_new(key.c_str()); auto* key_lbl = gtk_label_new(key.c_str());
gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f); gtk_label_set_xalign(GTK_LABEL(key_lbl), 1.0f);
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, row, 1, 1); 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); auto* val_label = gtk_label_new(nullptr);
gtk_label_set_xalign(GTK_LABEL(val_label), 0.0f); gtk_label_set_xalign(GTK_LABEL(val_label), 0.0f);
gtk_label_set_selectable(GTK_LABEL(val_label), TRUE); gtk_label_set_selectable(GTK_LABEL(val_label), TRUE);
@@ -287,26 +314,22 @@ void ConfigView::update_resolved_labels() {
if (display == "(unresolved)") { if (display == "(unresolved)") {
gtk_label_set_markup(GTK_LABEL(val_label), gtk_label_set_markup(GTK_LABEL(val_label),
"<span foreground=\"red\">(unresolved)</span>"); "<span foreground=\"#cc0000\" style=\"italic\">(unresolved)</span>");
} else if (std::filesystem::is_directory(display)) { } else if (path_ok) {
auto markup = "<span foreground=\"green\">" + display + "</span>"; gtk_label_set_text(GTK_LABEL(val_label), display.c_str());
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());
} else { } 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_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++; row++;
} }
if (row == 0) { if (row == 0) {
auto* lbl = gtk_label_new("No resolvable paths in config."); auto* lbl = gtk_label_new("No resolvable paths in config.");
gtk_label_set_xalign(GTK_LABEL(lbl), 0.0f); 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() { void ConfigView::update_config_buttons() {
bool has_config = !project_.config_path.empty(); bool has_config = !project_.config_path.empty();
if (has_config) { 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()); gtk_label_set_markup(GTK_LABEL(config_label_), markup.c_str());
} else { } 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_open_, !has_config);
gtk_widget_set_visible(btn_create_, !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); gtk_box_append(GTK_BOX(task_controls_), scroll);
// Task action buttons — grouped by function // 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); 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); auto* task_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(task_edit_group, "linked"); 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); auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(move_group, "linked"); gtk_widget_add_css_class(move_group, "linked");
auto* btn_up = gtk_button_new_with_label("Up"); auto* btn_up = gtk_button_new_with_label("Move Up");
auto* btn_down = gtk_button_new_with_label("Down"); 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_up);
gtk_box_append(GTK_BOX(move_group), btn_down); gtk_box_append(GTK_BOX(move_group), btn_down);
gtk_box_append(GTK_BOX(btn_box), move_group); 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"); 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(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_box_append(GTK_BOX(left), task_controls_);
gtk_paned_set_start_child(GTK_PANED(root_), left); gtk_paned_set_start_child(GTK_PANED(root_), left);
@@ -136,6 +145,11 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
} }
}), this); }), this);
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<PlanView*>(d);
self->refresh();
}), this);
update_plan_buttons(); update_plan_buttons();
} }
@@ -461,11 +475,19 @@ void PlanView::refresh() {
// reload units if paths now resolve // reload units if paths now resolve
project_.load_all_units(); project_.load_all_units();
// reload the plan from disk if one is loaded
auto* plan = current_plan(); 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()); 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"); gtk_label_set_markup(GTK_LABEL(plan_label_), "<b>Plan:</b> No plan loaded");
}
populate_task_list(); populate_task_list();
update_plan_buttons(); update_plan_buttons();

View File

@@ -17,14 +17,9 @@
*/ */
#include "views/shells_view.h" #include "views/shells_view.h"
#include <algorithm>
#include <cstring> #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 { namespace grex {
ShellsView::ShellsView(Project& project) : project_(project) { ShellsView::ShellsView(Project& project) : project_(project) {
@@ -50,11 +45,15 @@ ShellsView::ShellsView(Project& project) : project_(project) {
gtk_widget_add_css_class(file_scroll, "frame"); gtk_widget_add_css_class(file_scroll, "frame");
file_listbox_ = gtk_list_box_new(); file_listbox_ = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(file_listbox_), GTK_SELECTION_SINGLE); 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_scrolled_window_set_child(GTK_SCROLLED_WINDOW(file_scroll), file_listbox_);
gtk_box_append(GTK_BOX(left), file_scroll); 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); 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); auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(file_edit_group, "linked"); 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"); btn_save_ = gtk_button_new_with_label("Save File");
gtk_box_append(GTK_BOX(file_btn_box), btn_save_); 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_start_child(GTK_PANED(root_), left);
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE); 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); gtk_box_append(GTK_BOX(right), shell_scroll);
// Shell action buttons // Shell action buttons
auto* shell_ctrl_frame = gtk_frame_new("Shell Controls");
auto* shell_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); 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); auto* shell_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(shell_edit_group, "linked"); 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); auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(move_group, "linked"); gtk_widget_add_css_class(move_group, "linked");
auto* btn_move_up = gtk_button_new_with_label("Up"); auto* btn_move_up = gtk_button_new_with_label("Move Up");
auto* btn_move_down = gtk_button_new_with_label("Down"); 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_up);
gtk_box_append(GTK_BOX(move_group), btn_move_down); 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(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 // Shell properties editor
gtk_box_append(GTK_BOX(right), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); 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_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_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) { g_signal_connect(shell_listbox_, "row-selected", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer d) {
auto* self = static_cast<ShellsView*>(d); auto* self = static_cast<ShellsView*>(d);
if (!row) { self->clear_editor(); return; } 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) { g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<ShellsView*>(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"); self->project_.report_status("Error: no shell file selected");
return; return;
} }
auto& sf = self->project_.shell_files[self->current_file_idx_]; auto& sf = *self->selected_file_;
try { try {
sf.save(); sf.save();
self->file_dirty_ = false; self->file_dirty_ = false;
@@ -182,9 +196,9 @@ ShellsView::ShellsView(Project& project) : project_(project) {
auto bind = [this](GtkWidget* entry) { auto bind = [this](GtkWidget* entry) {
g_signal_connect(entry, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) { g_signal_connect(entry, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) {
auto* self = static_cast<ShellsView*>(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; 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; if (self->current_shell_idx_ >= (int)sf.shells.size()) return;
auto& s = sf.shells[self->current_shell_idx_]; auto& s = sf.shells[self->current_shell_idx_];
auto text = std::string(gtk_editable_get_text(e)); 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"); 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() { void ShellsView::populate_file_list() {
GtkWidget* child; GtkWidget* child;
while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr) while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr)
gtk_list_box_remove(GTK_LIST_BOX(file_listbox_), child); 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) { for (auto& sf : project_.shell_files) {
auto* row = gtk_list_box_row_new(); 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 text = std::string("\u25C6 ") + sf.filepath.filename().string();
auto* label = gtk_label_new(text.c_str()); auto* label = gtk_label_new(text.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 0.0f); gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
@@ -246,16 +274,15 @@ void ShellsView::populate_shell_list() {
current_shell_idx_ = -1; current_shell_idx_ = -1;
clear_editor(); 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>"); gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
return; return;
} }
auto& sf = project_.shell_files[current_file_idx_];
gtk_label_set_markup(GTK_LABEL(file_label_), 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* row = gtk_list_box_row_new();
auto text = std::string("\u25B8 ") + s.name; auto text = std::string("\u25B8 ") + s.name;
auto* label = gtk_label_new(text.c_str()); auto* label = gtk_label_new(text.c_str());
@@ -280,11 +307,11 @@ void ShellsView::clear_editor() {
} }
void ShellsView::load_shell(int idx) { void ShellsView::load_shell(int idx) {
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) { if (!selected_file_) {
clear_editor(); clear_editor();
return; return;
} }
auto& sf = project_.shell_files[current_file_idx_]; auto& sf = *selected_file_;
if (idx < 0 || idx >= (int)sf.shells.size()) { if (idx < 0 || idx >= (int)sf.shells.size()) {
clear_editor(); clear_editor();
return; return;
@@ -308,11 +335,11 @@ void ShellsView::refresh() {
void ShellsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data) { void ShellsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data) {
auto* self = static_cast<ShellsView*>(data); auto* self = static_cast<ShellsView*>(data);
if (!row) { if (!row) {
self->current_file_idx_ = -1; self->selected_file_ = nullptr;
self->populate_shell_list(); self->populate_shell_list();
return; 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; self->file_dirty_ = false;
gtk_widget_remove_css_class(self->btn_save_, "suggested-action"); gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
self->populate_shell_list(); 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->project_.shell_files.push_back(std::move(sf));
self->populate_file_list(); self->populate_file_list();
int last = (int)self->project_.shell_files.size() - 1; // Select the new file by matching filepath
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), last); for (auto& f : self->project_.shell_files) {
if (row) if (f.filepath == fp) {
gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row); 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()); self->project_.report_status("Created shell file: " + fp.filename().string());
} catch (const std::exception& e) { } 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) { void ShellsView::on_delete_file(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(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_) return;
return;
auto& sf = self->project_.shell_files[self->current_file_idx_]; auto name = self->selected_file_->filepath.filename().string();
auto name = sf.filepath.filename().string();
self->project_.shell_files.erase(self->project_.shell_files.begin() + self->current_file_idx_); auto it = std::find_if(self->project_.shell_files.begin(), self->project_.shell_files.end(),
self->current_file_idx_ = -1; [&](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->populate_file_list();
self->project_.report_status("Removed shell file from project: " + name + " (file not deleted from disk)"); self->project_.report_status("Removed shell file from project: " + name + " (file not deleted from disk)");
} }
void ShellsView::on_new_shell(GtkButton*, gpointer data) { void ShellsView::on_new_shell(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(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"); self->project_.report_status("Error: select a shell file first");
return; return;
} }
auto& sf = self->project_.shell_files[self->current_file_idx_]; auto& sf = *self->selected_file_;
ShellDef s; ShellDef s;
s.name = "new_shell"; s.name = "new_shell";
s.path = "/usr/bin/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) { void ShellsView::on_delete_shell(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
if (!row) return; if (!row) return;
int idx = gtk_list_box_row_get_index(row); 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; if (idx < 0 || idx >= (int)sf.shells.size()) return;
auto name = sf.shells[idx].name; 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) { void ShellsView::on_move_up(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
if (!row) return; if (!row) return;
int idx = gtk_list_box_row_get_index(row); 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; if (idx <= 0) return;
std::swap(sf.shells[idx], sf.shells[idx - 1]); 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) { void ShellsView::on_move_down(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
if (!row) return; if (!row) return;
int idx = gtk_list_box_row_get_index(row); 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; if (idx < 0 || idx >= (int)sf.shells.size() - 1) return;
std::swap(sf.shells[idx], sf.shells[idx + 1]); std::swap(sf.shells[idx], sf.shells[idx + 1]);

View File

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

View File

@@ -19,15 +19,10 @@
#include "views/units_view.h" #include "views/units_view.h"
#include "util/unsaved_dialog.h" #include "util/unsaved_dialog.h"
#include "util/unit_properties_dialog.h" #include "util/unit_properties_dialog.h"
#include <algorithm>
#include <cstring> #include <cstring>
#include <fstream> #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 { namespace grex {
UnitsView::UnitsView(Project& project, GrexConfig& grex_config) 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"); gtk_widget_add_css_class(file_scroll, "frame");
file_listbox_ = gtk_list_box_new(); file_listbox_ = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(file_listbox_), GTK_SELECTION_SINGLE); 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_scrolled_window_set_child(GTK_SCROLLED_WINDOW(file_scroll), file_listbox_);
gtk_box_append(GTK_BOX(left), file_scroll); gtk_box_append(GTK_BOX(left), file_scroll);
// File lifecycle buttons — grouped // File lifecycle buttons — grouped
auto* file_ctrl_frame = gtk_frame_new("File Controls");
auto* file_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); 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); auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(file_edit_group, "linked"); 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"); btn_save_ = gtk_button_new_with_label("Save File");
gtk_box_append(GTK_BOX(file_btn_box), btn_save_); 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_start_child(GTK_PANED(root_), left);
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE); 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); gtk_box_append(GTK_BOX(right), unit_scroll);
// Unit action buttons — grouped by function // 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); 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); auto* unit_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(unit_edit_group, "linked"); 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); auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
gtk_widget_add_css_class(move_group, "linked"); gtk_widget_add_css_class(move_group, "linked");
auto* btn_move_up = gtk_button_new_with_label("Up"); auto* btn_move_up = gtk_button_new_with_label("Move Up");
auto* btn_move_down = gtk_button_new_with_label("Down"); 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_up);
gtk_box_append(GTK_BOX(move_group), btn_move_down); 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(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_end_child(GTK_PANED(root_), right);
gtk_paned_set_shrink_end_child(GTK_PANED(root_), FALSE); 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(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(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) { g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<UnitsView*>(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"); self->project_.report_status("Error: no unit file selected");
return; return;
} }
auto& uf = self->project_.unit_files[self->current_file_idx_]; auto& uf = *self->selected_file_;
// Check for cross-file duplicates before saving // Check for cross-file duplicates before saving
for (auto& u : uf.units) { for (auto& u : uf.units) {
if (self->project_.is_unit_name_taken(u.name, &u)) { if (self->project_.is_unit_name_taken(u.name, &u)) {
@@ -162,15 +176,29 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
}), this); }), 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() { void UnitsView::populate_file_list() {
GtkWidget* child; GtkWidget* child;
while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr) while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr)
gtk_list_box_remove(GTK_LIST_BOX(file_listbox_), child); 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) { for (auto& uf : project_.unit_files) {
auto* row = gtk_list_box_row_new(); 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 text = std::string("\u25C6 ") + uf.filepath.filename().string();
auto* label = gtk_label_new(text.c_str()); auto* label = gtk_label_new(text.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 0.0f); 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) while ((child = gtk_widget_get_first_child(unit_listbox_)) != nullptr)
gtk_list_box_remove(GTK_LIST_BOX(unit_listbox_), child); 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>"); gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
return; return;
} }
auto& uf = project_.unit_files[current_file_idx_];
gtk_label_set_markup(GTK_LABEL(file_label_), 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* row = gtk_list_box_row_new();
auto text = std::string("\u2022 ") + u.name; auto text = std::string("\u2022 ") + u.name;
auto* label = gtk_label_new(text.c_str()); auto* label = gtk_label_new(text.c_str());
@@ -219,14 +246,12 @@ void UnitsView::refresh() {
} }
void UnitsView::save_current_file() { void UnitsView::save_current_file() {
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.unit_files.size()) if (!selected_file_) return;
return;
auto& uf = project_.unit_files[current_file_idx_];
try { try {
uf.save(); selected_file_->save();
file_dirty_ = false; file_dirty_ = false;
gtk_widget_remove_css_class(btn_save_, "suggested-action"); 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) { } catch (const std::exception& e) {
project_.report_status(std::string("Error: ") + e.what()); 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 (self->suppress_selection_) return;
if (!row) { if (!row) {
self->current_file_idx_ = -1; self->selected_file_ = nullptr;
self->populate_unit_list(); self->populate_unit_list();
return; 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_) { if (self->file_dirty_ && self->selected_file_ && new_file != self->selected_file_) {
int old_idx = self->current_file_idx_; // Snap selection back to the old file while showing the dialog
self->suppress_selection_ = true; 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); if (old_row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), old_row);
self->suppress_selection_ = false; self->suppress_selection_ = false;
@@ -256,31 +281,28 @@ void UnitsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data)
if (result == UnsavedResult::Cancel) if (result == UnsavedResult::Cancel)
return; return;
if (result == UnsavedResult::Save) { if (result == UnsavedResult::Save) {
auto& uf = self->project_.unit_files[self->current_file_idx_]; try { self->selected_file_->save(); } catch (const std::exception& e) {
try { uf.save(); } catch (const std::exception& e) {
self->project_.report_status(std::string("Error: ") + e.what()); self->project_.report_status(std::string("Error: ") + e.what());
} }
} else { } else {
auto idx = self->current_file_idx_; try {
if (idx >= 0 && idx < (int)self->project_.unit_files.size()) { *self->selected_file_ = UnitFile::load(self->selected_file_->filepath);
auto& uf = self->project_.unit_files[idx]; } catch (const std::exception& e) {
try { uf = UnitFile::load(uf.filepath); } catch (const std::exception& e) { self->project_.report_status(std::string("Error: ") + e.what());
self->project_.report_status(std::string("Error: ") + e.what());
}
} }
} }
self->file_dirty_ = false; gtk_widget_remove_css_class(self->btn_save_, "suggested-action"); self->file_dirty_ = false; gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
// Now snap to the new selection
self->suppress_selection_ = true; self->suppress_selection_ = true;
auto* target_row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), new_idx); gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
if (target_row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), target_row);
self->suppress_selection_ = false; self->suppress_selection_ = false;
self->current_file_idx_ = new_idx; self->selected_file_ = new_file;
self->populate_unit_list(); self->populate_unit_list();
return; 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->file_dirty_ = false; gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
self->populate_unit_list(); 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->project_.unit_files.push_back(std::move(uf));
self->populate_file_list(); self->populate_file_list();
// Select the new file // Select the new file by matching filepath
int last = (int)self->project_.unit_files.size() - 1; for (auto& f : self->project_.unit_files) {
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), last); if (f.filepath == fp) {
if (row) auto* row = self->find_file_row(&f);
gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row); 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()); self->project_.report_status("Created unit file: " + fp.filename().string());
} catch (const std::exception& e) { } 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) { void UnitsView::on_delete_file(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(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_) return;
return;
auto& uf = self->project_.unit_files[self->current_file_idx_]; auto name = self->selected_file_->filepath.filename().string();
auto name = uf.filepath.filename().string();
self->project_.unit_files.erase(self->project_.unit_files.begin() + self->current_file_idx_); auto it = std::find_if(self->project_.unit_files.begin(), self->project_.unit_files.end(),
self->current_file_idx_ = -1; [&](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->populate_file_list();
self->project_.report_status("Removed unit file from project: " + name + " (file not deleted from disk)"); self->project_.report_status("Removed unit file from project: " + name + " (file not deleted from disk)");
} }
void UnitsView::on_new_unit(GtkButton*, gpointer data) { void UnitsView::on_new_unit(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(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"); self->project_.report_status("Error: select a unit file first");
return; return;
} }
@@ -435,7 +462,7 @@ void UnitsView::on_new_unit(GtkButton*, gpointer data) {
return; return;
} }
auto& uf = nd->view->project_.unit_files[nd->view->current_file_idx_]; auto& uf = *nd->view->selected_file_;
Unit u; Unit u;
u.name = name; u.name = name;
@@ -453,14 +480,13 @@ void UnitsView::on_new_unit(GtkButton*, gpointer data) {
void UnitsView::on_delete_unit(GtkButton*, gpointer data) { void UnitsView::on_delete_unit(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) return; if (!row) return;
int idx = gtk_list_box_row_get_index(row); 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; if (idx < 0 || idx >= (int)uf.units.size()) return;
auto name = uf.units[idx].name; 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) { void UnitsView::on_unit_activated(GtkListBox*, GtkListBoxRow* row, gpointer data) {
auto* self = static_cast<UnitsView*>(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_) return;
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) // Get unit name from the row's label (strip bullet prefix)
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row)); 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) { void UnitsView::on_move_up(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) return; 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 // Find unit index by name from the row label
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row)); 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) { void UnitsView::on_move_down(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) return; 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 // Find unit index by name from the row label
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row)); 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) { void UnitsView::on_edit_unit(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(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_) return;
return;
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_)); auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->unit_listbox_));
if (!row) { if (!row) {
@@ -583,7 +605,7 @@ void UnitsView::on_edit_unit(GtkButton*, gpointer data) {
label_text = label_text.substr(strlen("\u2022 ")); label_text = label_text.substr(strlen("\u2022 "));
// Find unit by name // Find unit by name
auto& uf = self->project_.unit_files[self->current_file_idx_]; auto& uf = *self->selected_file_;
Unit* unit = nullptr; Unit* unit = nullptr;
for (auto& u : uf.units) { for (auto& u : uf.units) {
if (u.name == label_text) { unit = &u; break; } if (u.name == label_text) { unit = &u; break; }

View File

@@ -40,12 +40,13 @@ private:
GtkWidget* file_label_; GtkWidget* file_label_;
GtkWidget* btn_save_; GtkWidget* btn_save_;
int current_file_idx_ = -1; UnitFile* selected_file_ = nullptr;
bool file_dirty_ = false; bool file_dirty_ = false;
bool suppress_selection_ = false; bool suppress_selection_ = false;
void populate_file_list(); void populate_file_list();
void populate_unit_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_file_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);
static void on_new_file(GtkButton* btn, gpointer data); static void on_new_file(GtkButton* btn, gpointer data);