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

@@ -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; }