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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user