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:
@@ -166,19 +166,32 @@ void Project::load_plan(const fs::path& plan_path) {
|
||||
|
||||
void Project::reload_shells() {
|
||||
auto sp = resolved_shells_path();
|
||||
if (sp.empty()) return;
|
||||
if (!fs::is_directory(sp)) return;
|
||||
if (sp.empty()) {
|
||||
report_status("Error: shells path not resolved");
|
||||
return;
|
||||
}
|
||||
if (!fs::is_directory(sp)) {
|
||||
report_status("Error: shells path is not a directory: " + sp.string());
|
||||
return;
|
||||
}
|
||||
|
||||
shell_files.clear();
|
||||
int file_count = 0;
|
||||
int total_shells = 0;
|
||||
for (auto& entry : fs::directory_iterator(sp)) {
|
||||
if (entry.path().extension() == ".shells") {
|
||||
try {
|
||||
shell_files.push_back(ShellsFile::load(entry.path()));
|
||||
auto sf = ShellsFile::load(entry.path());
|
||||
total_shells += (int)sf.shells.size();
|
||||
file_count++;
|
||||
shell_files.push_back(std::move(sf));
|
||||
} catch (const std::exception& e) {
|
||||
report_status("Error loading shells: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
report_status("Loaded " + std::to_string(total_shells) + " shells from " +
|
||||
std::to_string(file_count) + " files at '" + sp.string() + "'");
|
||||
}
|
||||
|
||||
std::vector<ShellDef> Project::all_shells() const {
|
||||
|
||||
@@ -39,15 +39,23 @@ ConfigView::ConfigView(Project& project) : project_(project) {
|
||||
root_ = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(root_), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
|
||||
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
gtk_widget_set_margin_start(box, 16);
|
||||
gtk_widget_set_margin_end(box, 16);
|
||||
gtk_widget_set_margin_top(box, 16);
|
||||
gtk_widget_set_margin_bottom(box, 16);
|
||||
// Outer centering wrapper — keeps form from stretching on wide screens
|
||||
auto* outer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_set_vexpand(outer, TRUE);
|
||||
|
||||
// === Config file label + Open/Close buttons ===
|
||||
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
|
||||
gtk_widget_set_margin_start(box, 48);
|
||||
gtk_widget_set_margin_end(box, 48);
|
||||
gtk_widget_set_margin_top(box, 24);
|
||||
gtk_widget_set_margin_bottom(box, 24);
|
||||
gtk_widget_set_hexpand(box, TRUE);
|
||||
gtk_widget_set_size_request(box, -1, -1);
|
||||
|
||||
// === Config file header ===
|
||||
config_label_ = gtk_label_new(nullptr);
|
||||
gtk_label_set_xalign(GTK_LABEL(config_label_), 0.0f);
|
||||
gtk_label_set_selectable(GTK_LABEL(config_label_), TRUE);
|
||||
gtk_widget_set_margin_bottom(config_label_, 4);
|
||||
gtk_box_append(GTK_BOX(box), config_label_);
|
||||
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
@@ -68,63 +76,61 @@ ConfigView::ConfigView(Project& project) : project_(project) {
|
||||
g_signal_connect(btn_close_, "clicked", G_CALLBACK(on_close_config), this);
|
||||
|
||||
// === Config content — greyed out when no config loaded ===
|
||||
config_content_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||||
|
||||
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// === Config fields section ===
|
||||
auto* config_header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(config_header), "<b>Configuration</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(config_header), 0.0f);
|
||||
gtk_box_append(GTK_BOX(config_content_), config_header);
|
||||
config_content_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
|
||||
|
||||
// === Configuration fields ===
|
||||
auto* config_frame = gtk_frame_new("Configuration");
|
||||
config_grid_ = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(config_grid_), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(config_grid_), 12);
|
||||
gtk_box_append(GTK_BOX(config_content_), config_grid_);
|
||||
gtk_grid_set_row_spacing(GTK_GRID(config_grid_), 10);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(config_grid_), 16);
|
||||
gtk_widget_set_margin_start(config_grid_, 12);
|
||||
gtk_widget_set_margin_end(config_grid_, 12);
|
||||
gtk_widget_set_margin_top(config_grid_, 12);
|
||||
gtk_widget_set_margin_bottom(config_grid_, 12);
|
||||
gtk_frame_set_child(GTK_FRAME(config_frame), config_grid_);
|
||||
gtk_box_append(GTK_BOX(config_content_), config_frame);
|
||||
|
||||
build_config_fields();
|
||||
|
||||
// === Resolved paths section ===
|
||||
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
auto* resolved_header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(resolved_header), "<b>Resolved Paths</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(resolved_header), 0.0f);
|
||||
gtk_box_append(GTK_BOX(config_content_), resolved_header);
|
||||
|
||||
// === Resolved Paths ===
|
||||
auto* resolved_frame = gtk_frame_new("Resolved Paths");
|
||||
resolved_grid_ = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(resolved_grid_), 4);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(resolved_grid_), 12);
|
||||
gtk_box_append(GTK_BOX(config_content_), resolved_grid_);
|
||||
gtk_grid_set_row_spacing(GTK_GRID(resolved_grid_), 6);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(resolved_grid_), 16);
|
||||
gtk_widget_set_margin_start(resolved_grid_, 12);
|
||||
gtk_widget_set_margin_end(resolved_grid_, 12);
|
||||
gtk_widget_set_margin_top(resolved_grid_, 12);
|
||||
gtk_widget_set_margin_bottom(resolved_grid_, 12);
|
||||
gtk_frame_set_child(GTK_FRAME(resolved_frame), resolved_grid_);
|
||||
gtk_box_append(GTK_BOX(config_content_), resolved_frame);
|
||||
|
||||
// === Variables section ===
|
||||
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
auto* vars_header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(vars_header), "<b>Variables</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(vars_header), 0.0f);
|
||||
gtk_box_append(GTK_BOX(config_content_), vars_header);
|
||||
// === Variables ===
|
||||
auto* vars_frame = gtk_frame_new("Variables");
|
||||
auto* vars_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(vars_box, 12);
|
||||
gtk_widget_set_margin_end(vars_box, 12);
|
||||
gtk_widget_set_margin_top(vars_box, 12);
|
||||
gtk_widget_set_margin_bottom(vars_box, 12);
|
||||
|
||||
auto* vars_desc = gtk_label_new("Set values for variables found in config fields. Environment variables are used automatically.");
|
||||
gtk_label_set_xalign(GTK_LABEL(vars_desc), 0.0f);
|
||||
gtk_label_set_wrap(GTK_LABEL(vars_desc), TRUE);
|
||||
gtk_widget_add_css_class(vars_desc, "dim-label");
|
||||
gtk_box_append(GTK_BOX(config_content_), vars_desc);
|
||||
gtk_box_append(GTK_BOX(vars_box), vars_desc);
|
||||
|
||||
vars_grid_ = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(vars_grid_), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 12);
|
||||
gtk_box_append(GTK_BOX(config_content_), vars_grid_);
|
||||
gtk_grid_set_row_spacing(GTK_GRID(vars_grid_), 10);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 16);
|
||||
gtk_box_append(GTK_BOX(vars_box), vars_grid_);
|
||||
|
||||
gtk_frame_set_child(GTK_FRAME(vars_frame), vars_box);
|
||||
gtk_box_append(GTK_BOX(config_content_), vars_frame);
|
||||
|
||||
build_variables_section();
|
||||
update_resolved_labels();
|
||||
|
||||
// === Save button ===
|
||||
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// === Save button — full width for presence ===
|
||||
btn_save_ = gtk_button_new_with_label("Save Config");
|
||||
gtk_widget_set_halign(btn_save_, GTK_ALIGN_END);
|
||||
gtk_box_append(GTK_BOX(config_content_), btn_save_);
|
||||
|
||||
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
@@ -132,8 +138,9 @@ ConfigView::ConfigView(Project& project) : project_(project) {
|
||||
}), this);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), config_content_);
|
||||
gtk_box_append(GTK_BOX(outer), box);
|
||||
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), box);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), outer);
|
||||
|
||||
update_config_buttons();
|
||||
}
|
||||
@@ -153,6 +160,8 @@ void ConfigView::build_config_fields() {
|
||||
for (auto& [key, val] : project_.config.data().items()) {
|
||||
auto* label = gtk_label_new(key.c_str());
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 1.0f);
|
||||
gtk_widget_set_size_request(label, 140, -1);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(config_grid_), label, 0, row, 1, 1);
|
||||
|
||||
auto* entry = gtk_entry_new();
|
||||
@@ -209,6 +218,8 @@ void ConfigView::build_variables_section() {
|
||||
auto var_label = "${" + name + "}";
|
||||
auto* lbl = gtk_label_new(var_label.c_str());
|
||||
gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f);
|
||||
gtk_widget_set_size_request(lbl, 140, -1);
|
||||
gtk_widget_add_css_class(lbl, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(vars_grid_), lbl, 0, row, 1, 1);
|
||||
|
||||
auto* entry = gtk_entry_new();
|
||||
@@ -277,9 +288,25 @@ void ConfigView::update_resolved_labels() {
|
||||
display = resolved;
|
||||
}
|
||||
|
||||
auto* lbl = gtk_label_new(key.c_str());
|
||||
gtk_label_set_xalign(GTK_LABEL(lbl), 1.0f);
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, row, 1, 1);
|
||||
auto* key_lbl = gtk_label_new(key.c_str());
|
||||
gtk_label_set_xalign(GTK_LABEL(key_lbl), 1.0f);
|
||||
gtk_widget_set_size_request(key_lbl, 140, -1);
|
||||
gtk_widget_add_css_class(key_lbl, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), key_lbl, 0, row, 1, 1);
|
||||
|
||||
// Status indicator
|
||||
bool path_ok = (display != "(unresolved)") &&
|
||||
(std::filesystem::exists(display) || std::filesystem::is_directory(display));
|
||||
auto* indicator = gtk_label_new(nullptr);
|
||||
gtk_label_set_xalign(GTK_LABEL(indicator), 0.5f);
|
||||
if (display == "(unresolved)")
|
||||
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#cc0000\">\u2718</span>");
|
||||
else if (path_ok)
|
||||
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#4e9a06\">\u2714</span>");
|
||||
else
|
||||
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#cc0000\">\u2718</span>");
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), indicator, 1, row, 1, 1);
|
||||
|
||||
auto* val_label = gtk_label_new(nullptr);
|
||||
gtk_label_set_xalign(GTK_LABEL(val_label), 0.0f);
|
||||
gtk_label_set_selectable(GTK_LABEL(val_label), TRUE);
|
||||
@@ -287,26 +314,22 @@ void ConfigView::update_resolved_labels() {
|
||||
|
||||
if (display == "(unresolved)") {
|
||||
gtk_label_set_markup(GTK_LABEL(val_label),
|
||||
"<span foreground=\"red\">(unresolved)</span>");
|
||||
} else if (std::filesystem::is_directory(display)) {
|
||||
auto markup = "<span foreground=\"green\">" + display + "</span>";
|
||||
gtk_label_set_markup(GTK_LABEL(val_label), markup.c_str());
|
||||
} else if (std::filesystem::exists(display)) {
|
||||
auto markup = "<span foreground=\"green\">" + display + "</span>";
|
||||
gtk_label_set_markup(GTK_LABEL(val_label), markup.c_str());
|
||||
"<span foreground=\"#cc0000\" style=\"italic\">(unresolved)</span>");
|
||||
} else if (path_ok) {
|
||||
gtk_label_set_text(GTK_LABEL(val_label), display.c_str());
|
||||
} else {
|
||||
auto markup = "<span foreground=\"red\">" + display + "</span>";
|
||||
auto markup = "<span foreground=\"#cc0000\">" + display + "</span>";
|
||||
gtk_label_set_markup(GTK_LABEL(val_label), markup.c_str());
|
||||
}
|
||||
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), val_label, 1, row, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), val_label, 2, row, 1, 1);
|
||||
row++;
|
||||
}
|
||||
|
||||
if (row == 0) {
|
||||
auto* lbl = gtk_label_new("No resolvable paths in config.");
|
||||
gtk_label_set_xalign(GTK_LABEL(lbl), 0.0f);
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, 0, 2, 1);
|
||||
gtk_grid_attach(GTK_GRID(resolved_grid_), lbl, 0, 0, 3, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,10 +361,15 @@ void ConfigView::apply_config() {
|
||||
void ConfigView::update_config_buttons() {
|
||||
bool has_config = !project_.config_path.empty();
|
||||
if (has_config) {
|
||||
auto markup = std::string("<b>Current Rex Config:</b> ") + project_.config_path.filename().string();
|
||||
auto markup = std::string("<span size=\"large\" weight=\"bold\">Rex Config: ") +
|
||||
project_.config_path.filename().string() + "</span>\n" +
|
||||
"<span size=\"small\" alpha=\"60%\">" +
|
||||
project_.config_path.string() + "</span>";
|
||||
gtk_label_set_markup(GTK_LABEL(config_label_), markup.c_str());
|
||||
} else {
|
||||
gtk_label_set_markup(GTK_LABEL(config_label_), "<b>Current Rex Config:</b> No config loaded");
|
||||
gtk_label_set_markup(GTK_LABEL(config_label_),
|
||||
"<span size=\"large\" weight=\"bold\">Rex Config</span>\n"
|
||||
"<span size=\"small\" alpha=\"60%\">No config loaded</span>");
|
||||
}
|
||||
gtk_widget_set_visible(btn_open_, !has_config);
|
||||
gtk_widget_set_visible(btn_create_, !has_config);
|
||||
|
||||
@@ -74,7 +74,12 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
|
||||
gtk_box_append(GTK_BOX(task_controls_), scroll);
|
||||
|
||||
// Task action buttons — grouped by function
|
||||
auto* plan_ctrl_frame = gtk_frame_new("Plan Controls");
|
||||
auto* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_start(btn_box, 8);
|
||||
gtk_widget_set_margin_end(btn_box, 8);
|
||||
gtk_widget_set_margin_top(btn_box, 8);
|
||||
gtk_widget_set_margin_bottom(btn_box, 8);
|
||||
|
||||
auto* task_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(task_edit_group, "linked");
|
||||
@@ -86,8 +91,8 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
|
||||
|
||||
auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(move_group, "linked");
|
||||
auto* btn_up = gtk_button_new_with_label("Up");
|
||||
auto* btn_down = gtk_button_new_with_label("Down");
|
||||
auto* btn_up = gtk_button_new_with_label("Move Up");
|
||||
auto* btn_down = gtk_button_new_with_label("Move Down");
|
||||
gtk_box_append(GTK_BOX(move_group), btn_up);
|
||||
gtk_box_append(GTK_BOX(move_group), btn_down);
|
||||
gtk_box_append(GTK_BOX(btn_box), move_group);
|
||||
@@ -95,7 +100,11 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
|
||||
btn_save_plan_ = gtk_button_new_with_label("Save Plan");
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_save_plan_);
|
||||
|
||||
gtk_box_append(GTK_BOX(task_controls_), btn_box);
|
||||
auto* btn_refresh = gtk_button_new_with_label("Refresh");
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_refresh);
|
||||
|
||||
gtk_frame_set_child(GTK_FRAME(plan_ctrl_frame), btn_box);
|
||||
gtk_box_append(GTK_BOX(task_controls_), plan_ctrl_frame);
|
||||
gtk_box_append(GTK_BOX(left), task_controls_);
|
||||
|
||||
gtk_paned_set_start_child(GTK_PANED(root_), left);
|
||||
@@ -136,6 +145,11 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
|
||||
}
|
||||
}), this);
|
||||
|
||||
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
auto* self = static_cast<PlanView*>(d);
|
||||
self->refresh();
|
||||
}), this);
|
||||
|
||||
update_plan_buttons();
|
||||
|
||||
}
|
||||
@@ -461,11 +475,19 @@ void PlanView::refresh() {
|
||||
// reload units if paths now resolve
|
||||
project_.load_all_units();
|
||||
|
||||
// reload the plan from disk if one is loaded
|
||||
auto* plan = current_plan();
|
||||
if (plan)
|
||||
if (plan) {
|
||||
try {
|
||||
*plan = Plan::load(plan->filepath);
|
||||
project_.report_status("Reloaded plan: " + plan->filepath.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
project_.report_status("Error reloading plan: " + std::string(e.what()));
|
||||
}
|
||||
gtk_label_set_markup(GTK_LABEL(plan_label_), (std::string("<b>Plan:</b> ") + plan->filepath.filename().string()).c_str());
|
||||
else
|
||||
} else {
|
||||
gtk_label_set_markup(GTK_LABEL(plan_label_), "<b>Plan:</b> No plan loaded");
|
||||
}
|
||||
|
||||
populate_task_list();
|
||||
update_plan_buttons();
|
||||
|
||||
@@ -17,14 +17,9 @@
|
||||
*/
|
||||
|
||||
#include "views/shells_view.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
static int sort_listbox_alpha(GtkListBoxRow* a, GtkListBoxRow* b, gpointer) {
|
||||
auto* la = GTK_LABEL(gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(a)));
|
||||
auto* lb = GTK_LABEL(gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(b)));
|
||||
return std::strcmp(gtk_label_get_text(la), gtk_label_get_text(lb));
|
||||
}
|
||||
|
||||
namespace grex {
|
||||
|
||||
ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
@@ -50,11 +45,15 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
gtk_widget_add_css_class(file_scroll, "frame");
|
||||
file_listbox_ = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(file_listbox_), GTK_SELECTION_SINGLE);
|
||||
gtk_list_box_set_sort_func(GTK_LIST_BOX(file_listbox_), sort_listbox_alpha, nullptr, nullptr);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(file_scroll), file_listbox_);
|
||||
gtk_box_append(GTK_BOX(left), file_scroll);
|
||||
|
||||
auto* file_ctrl_frame = gtk_frame_new("File Controls");
|
||||
auto* file_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_start(file_btn_box, 8);
|
||||
gtk_widget_set_margin_end(file_btn_box, 8);
|
||||
gtk_widget_set_margin_top(file_btn_box, 8);
|
||||
gtk_widget_set_margin_bottom(file_btn_box, 8);
|
||||
|
||||
auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(file_edit_group, "linked");
|
||||
@@ -67,7 +66,11 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
btn_save_ = gtk_button_new_with_label("Save File");
|
||||
gtk_box_append(GTK_BOX(file_btn_box), btn_save_);
|
||||
|
||||
gtk_box_append(GTK_BOX(left), file_btn_box);
|
||||
auto* btn_refresh = gtk_button_new_with_label("Refresh");
|
||||
gtk_box_append(GTK_BOX(file_btn_box), btn_refresh);
|
||||
|
||||
gtk_frame_set_child(GTK_FRAME(file_ctrl_frame), file_btn_box);
|
||||
gtk_box_append(GTK_BOX(left), file_ctrl_frame);
|
||||
|
||||
gtk_paned_set_start_child(GTK_PANED(root_), left);
|
||||
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE);
|
||||
@@ -94,7 +97,12 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
gtk_box_append(GTK_BOX(right), shell_scroll);
|
||||
|
||||
// Shell action buttons
|
||||
auto* shell_ctrl_frame = gtk_frame_new("Shell Controls");
|
||||
auto* shell_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_start(shell_btn_box, 8);
|
||||
gtk_widget_set_margin_end(shell_btn_box, 8);
|
||||
gtk_widget_set_margin_top(shell_btn_box, 8);
|
||||
gtk_widget_set_margin_bottom(shell_btn_box, 8);
|
||||
|
||||
auto* shell_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(shell_edit_group, "linked");
|
||||
@@ -106,13 +114,14 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
|
||||
auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(move_group, "linked");
|
||||
auto* btn_move_up = gtk_button_new_with_label("Up");
|
||||
auto* btn_move_down = gtk_button_new_with_label("Down");
|
||||
auto* btn_move_up = gtk_button_new_with_label("Move Up");
|
||||
auto* btn_move_down = gtk_button_new_with_label("Move Down");
|
||||
gtk_box_append(GTK_BOX(move_group), btn_move_up);
|
||||
gtk_box_append(GTK_BOX(move_group), btn_move_down);
|
||||
gtk_box_append(GTK_BOX(shell_btn_box), move_group);
|
||||
|
||||
gtk_box_append(GTK_BOX(right), shell_btn_box);
|
||||
gtk_frame_set_child(GTK_FRAME(shell_ctrl_frame), shell_btn_box);
|
||||
gtk_box_append(GTK_BOX(right), shell_ctrl_frame);
|
||||
|
||||
// Shell properties editor
|
||||
gtk_box_append(GTK_BOX(right), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
@@ -155,6 +164,11 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
g_signal_connect(btn_move_up, "clicked", G_CALLBACK(on_move_up), this);
|
||||
g_signal_connect(btn_move_down, "clicked", G_CALLBACK(on_move_down), this);
|
||||
|
||||
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
auto* self = static_cast<ShellsView*>(d);
|
||||
self->refresh();
|
||||
}), this);
|
||||
|
||||
g_signal_connect(shell_listbox_, "row-selected", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer d) {
|
||||
auto* self = static_cast<ShellsView*>(d);
|
||||
if (!row) { self->clear_editor(); return; }
|
||||
@@ -163,11 +177,11 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
|
||||
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
auto* self = static_cast<ShellsView*>(d);
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size()) {
|
||||
if (!self->selected_file_) {
|
||||
self->project_.report_status("Error: no shell file selected");
|
||||
return;
|
||||
}
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto& sf = *self->selected_file_;
|
||||
try {
|
||||
sf.save();
|
||||
self->file_dirty_ = false;
|
||||
@@ -182,9 +196,9 @@ ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
auto bind = [this](GtkWidget* entry) {
|
||||
g_signal_connect(entry, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) {
|
||||
auto* self = static_cast<ShellsView*>(d);
|
||||
if (self->loading_ || self->current_file_idx_ < 0 || self->current_shell_idx_ < 0)
|
||||
if (self->loading_ || !self->selected_file_ || self->current_shell_idx_ < 0)
|
||||
return;
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto& sf = *self->selected_file_;
|
||||
if (self->current_shell_idx_ >= (int)sf.shells.size()) return;
|
||||
auto& s = sf.shells[self->current_shell_idx_];
|
||||
auto text = std::string(gtk_editable_get_text(e));
|
||||
@@ -215,15 +229,29 @@ void ShellsView::mark_file_dirty() {
|
||||
gtk_widget_add_css_class(btn_save_, "suggested-action");
|
||||
}
|
||||
|
||||
GtkListBoxRow* ShellsView::find_file_row(ShellsFile* sf) {
|
||||
for (int i = 0; ; i++) {
|
||||
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(file_listbox_), i);
|
||||
if (!row) return nullptr;
|
||||
if (g_object_get_data(G_OBJECT(row), "shell-file") == sf) return row;
|
||||
}
|
||||
}
|
||||
|
||||
void ShellsView::populate_file_list() {
|
||||
GtkWidget* child;
|
||||
while ((child = gtk_widget_get_first_child(file_listbox_)) != nullptr)
|
||||
gtk_list_box_remove(GTK_LIST_BOX(file_listbox_), child);
|
||||
|
||||
current_file_idx_ = -1;
|
||||
selected_file_ = nullptr;
|
||||
|
||||
std::sort(project_.shell_files.begin(), project_.shell_files.end(),
|
||||
[](const ShellsFile& a, const ShellsFile& b) {
|
||||
return a.filepath.filename().string() < b.filepath.filename().string();
|
||||
});
|
||||
|
||||
for (auto& sf : project_.shell_files) {
|
||||
auto* row = gtk_list_box_row_new();
|
||||
g_object_set_data(G_OBJECT(row), "shell-file", &sf);
|
||||
auto text = std::string("\u25C6 ") + sf.filepath.filename().string();
|
||||
auto* label = gtk_label_new(text.c_str());
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
@@ -246,16 +274,15 @@ void ShellsView::populate_shell_list() {
|
||||
current_shell_idx_ = -1;
|
||||
clear_editor();
|
||||
|
||||
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) {
|
||||
if (!selected_file_) {
|
||||
gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& sf = project_.shell_files[current_file_idx_];
|
||||
gtk_label_set_markup(GTK_LABEL(file_label_),
|
||||
(std::string("<b>") + sf.filepath.filename().string() + "</b>").c_str());
|
||||
(std::string("<b>") + selected_file_->filepath.filename().string() + "</b>").c_str());
|
||||
|
||||
for (auto& s : sf.shells) {
|
||||
for (auto& s : selected_file_->shells) {
|
||||
auto* row = gtk_list_box_row_new();
|
||||
auto text = std::string("\u25B8 ") + s.name;
|
||||
auto* label = gtk_label_new(text.c_str());
|
||||
@@ -280,11 +307,11 @@ void ShellsView::clear_editor() {
|
||||
}
|
||||
|
||||
void ShellsView::load_shell(int idx) {
|
||||
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) {
|
||||
if (!selected_file_) {
|
||||
clear_editor();
|
||||
return;
|
||||
}
|
||||
auto& sf = project_.shell_files[current_file_idx_];
|
||||
auto& sf = *selected_file_;
|
||||
if (idx < 0 || idx >= (int)sf.shells.size()) {
|
||||
clear_editor();
|
||||
return;
|
||||
@@ -308,11 +335,11 @@ void ShellsView::refresh() {
|
||||
void ShellsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (!row) {
|
||||
self->current_file_idx_ = -1;
|
||||
self->selected_file_ = nullptr;
|
||||
self->populate_shell_list();
|
||||
return;
|
||||
}
|
||||
self->current_file_idx_ = gtk_list_box_row_get_index(row);
|
||||
self->selected_file_ = static_cast<ShellsFile*>(g_object_get_data(G_OBJECT(row), "shell-file"));
|
||||
self->file_dirty_ = false;
|
||||
gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
|
||||
self->populate_shell_list();
|
||||
@@ -371,10 +398,14 @@ void ShellsView::on_new_file_response(GObject* source, GAsyncResult* res, gpoint
|
||||
self->project_.shell_files.push_back(std::move(sf));
|
||||
self->populate_file_list();
|
||||
|
||||
int last = (int)self->project_.shell_files.size() - 1;
|
||||
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->file_listbox_), last);
|
||||
if (row)
|
||||
gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
|
||||
// Select the new file by matching filepath
|
||||
for (auto& f : self->project_.shell_files) {
|
||||
if (f.filepath == fp) {
|
||||
auto* row = self->find_file_row(&f);
|
||||
if (row) gtk_list_box_select_row(GTK_LIST_BOX(self->file_listbox_), row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self->project_.report_status("Created shell file: " + fp.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
@@ -384,26 +415,28 @@ void ShellsView::on_new_file_response(GObject* source, GAsyncResult* res, gpoint
|
||||
|
||||
void ShellsView::on_delete_file(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
|
||||
return;
|
||||
if (!self->selected_file_) return;
|
||||
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto name = sf.filepath.filename().string();
|
||||
auto name = self->selected_file_->filepath.filename().string();
|
||||
|
||||
self->project_.shell_files.erase(self->project_.shell_files.begin() + self->current_file_idx_);
|
||||
self->current_file_idx_ = -1;
|
||||
auto it = std::find_if(self->project_.shell_files.begin(), self->project_.shell_files.end(),
|
||||
[&](const ShellsFile& sf) { return &sf == self->selected_file_; });
|
||||
if (it != self->project_.shell_files.end())
|
||||
self->project_.shell_files.erase(it);
|
||||
|
||||
self->selected_file_ = nullptr;
|
||||
self->populate_file_list();
|
||||
self->project_.report_status("Removed shell file from project: " + name + " (file not deleted from disk)");
|
||||
}
|
||||
|
||||
void ShellsView::on_new_shell(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size()) {
|
||||
if (!self->selected_file_) {
|
||||
self->project_.report_status("Error: select a shell file first");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto& sf = *self->selected_file_;
|
||||
ShellDef s;
|
||||
s.name = "new_shell";
|
||||
s.path = "/usr/bin/new_shell";
|
||||
@@ -421,14 +454,13 @@ void ShellsView::on_new_shell(GtkButton*, gpointer data) {
|
||||
|
||||
void ShellsView::on_delete_shell(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
|
||||
return;
|
||||
if (!self->selected_file_) return;
|
||||
|
||||
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
|
||||
if (!row) return;
|
||||
|
||||
int idx = gtk_list_box_row_get_index(row);
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto& sf = *self->selected_file_;
|
||||
if (idx < 0 || idx >= (int)sf.shells.size()) return;
|
||||
|
||||
auto name = sf.shells[idx].name;
|
||||
@@ -440,14 +472,13 @@ void ShellsView::on_delete_shell(GtkButton*, gpointer data) {
|
||||
|
||||
void ShellsView::on_move_up(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
|
||||
return;
|
||||
if (!self->selected_file_) return;
|
||||
|
||||
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
|
||||
if (!row) return;
|
||||
|
||||
int idx = gtk_list_box_row_get_index(row);
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto& sf = *self->selected_file_;
|
||||
if (idx <= 0) return;
|
||||
|
||||
std::swap(sf.shells[idx], sf.shells[idx - 1]);
|
||||
@@ -460,14 +491,13 @@ void ShellsView::on_move_up(GtkButton*, gpointer data) {
|
||||
|
||||
void ShellsView::on_move_down(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
|
||||
return;
|
||||
if (!self->selected_file_) return;
|
||||
|
||||
auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(self->shell_listbox_));
|
||||
if (!row) return;
|
||||
|
||||
int idx = gtk_list_box_row_get_index(row);
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto& sf = *self->selected_file_;
|
||||
if (idx < 0 || idx >= (int)sf.shells.size() - 1) return;
|
||||
|
||||
std::swap(sf.shells[idx], sf.shells[idx + 1]);
|
||||
|
||||
@@ -47,7 +47,7 @@ private:
|
||||
GtkWidget* entry_exec_arg_;
|
||||
GtkWidget* entry_source_cmd_;
|
||||
|
||||
int current_file_idx_ = -1;
|
||||
ShellsFile* selected_file_ = nullptr;
|
||||
int current_shell_idx_ = -1;
|
||||
bool loading_ = false;
|
||||
bool file_dirty_ = false;
|
||||
@@ -57,6 +57,7 @@ private:
|
||||
void load_shell(int idx);
|
||||
void clear_editor();
|
||||
void mark_file_dirty();
|
||||
GtkListBoxRow* find_file_row(ShellsFile* sf);
|
||||
|
||||
static void on_file_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);
|
||||
static void on_new_file(GtkButton* btn, gpointer data);
|
||||
|
||||
@@ -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) {
|
||||
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; }
|
||||
|
||||
@@ -40,12 +40,13 @@ private:
|
||||
GtkWidget* file_label_;
|
||||
GtkWidget* btn_save_;
|
||||
|
||||
int current_file_idx_ = -1;
|
||||
UnitFile* selected_file_ = nullptr;
|
||||
bool file_dirty_ = false;
|
||||
bool suppress_selection_ = false;
|
||||
|
||||
void populate_file_list();
|
||||
void populate_unit_list();
|
||||
GtkListBoxRow* find_file_row(UnitFile* uf);
|
||||
|
||||
static void on_file_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);
|
||||
static void on_new_file(GtkButton* btn, gpointer data);
|
||||
|
||||
Reference in New Issue
Block a user