Beautify UI across all tabs, restructure shells to multi-file, polish unit dialog
- Reorder tabs: Rex Config, Units, Plans, Shells - Beautify Units, Plans, Shells, and Config tabs with framed lists, linked button groups, consistent margins, and dim labels - Grey out plan controls and task properties when no plan/task loaded - Restructure shells from single-file to multi-file directory model, paralleling the units architecture (create/delete/save files, create/delete/edit/move shells within files) - Fix shells loading to scan directories for .shells files - Beautify unit properties dialog with GtkFrame sections, dim-label field labels, internal padding, and linked file action buttons - Add rectifier Select/Open/Create file buttons - Fix GtkSwitch multi-click issue using state-set signal - Move Save File button to unit files sidebar - Sync unit editor state after name changes in properties dialog
This commit is contained in:
@@ -133,13 +133,17 @@ Project Project::load(const fs::path& cfg_path) {
|
||||
// scan all config string values for variables, populate from environment
|
||||
proj.resolver.scan_and_populate(proj.config.all_string_values());
|
||||
|
||||
// auto-load shells if path resolves
|
||||
// auto-load all shells files from shells directory
|
||||
auto sp = proj.resolved_shells_path();
|
||||
if (!sp.empty() && fs::exists(sp)) {
|
||||
try {
|
||||
proj.shells = ShellsFile::load(sp);
|
||||
} catch (const std::exception& e) {
|
||||
// no status_cb set yet at load time — caller handles
|
||||
if (!sp.empty() && fs::is_directory(sp)) {
|
||||
for (auto& entry : fs::directory_iterator(sp)) {
|
||||
if (entry.path().extension() == ".shells") {
|
||||
try {
|
||||
proj.shell_files.push_back(ShellsFile::load(entry.path()));
|
||||
} catch (const std::exception&) {
|
||||
// no status_cb set yet at load time
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,33 +164,28 @@ void Project::load_plan(const fs::path& plan_path) {
|
||||
}
|
||||
}
|
||||
|
||||
void Project::load_shells(const fs::path& shells_path) {
|
||||
shells = ShellsFile();
|
||||
try {
|
||||
shells = ShellsFile::load(shells_path);
|
||||
report_status("Loaded shells: " + shells_path.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
report_status("Error: failed to load shells: " + std::string(e.what()));
|
||||
throw;
|
||||
void Project::reload_shells() {
|
||||
auto sp = resolved_shells_path();
|
||||
if (sp.empty()) return;
|
||||
if (!fs::is_directory(sp)) return;
|
||||
|
||||
shell_files.clear();
|
||||
for (auto& entry : fs::directory_iterator(sp)) {
|
||||
if (entry.path().extension() == ".shells") {
|
||||
try {
|
||||
shell_files.push_back(ShellsFile::load(entry.path()));
|
||||
} catch (const std::exception& e) {
|
||||
report_status("Error loading shells: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Project::reload_shells() {
|
||||
auto sp = resolved_shells_path();
|
||||
if (sp.empty()) {
|
||||
report_status("Error: shells path not resolved");
|
||||
return;
|
||||
}
|
||||
if (!fs::exists(sp)) {
|
||||
report_status("Error: shells file not found: " + sp.string());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
shells = ShellsFile::load(sp);
|
||||
report_status("Loaded shells: " + sp.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
report_status("Error: failed to load shells: " + std::string(e.what()));
|
||||
}
|
||||
std::vector<ShellDef> Project::all_shells() const {
|
||||
std::vector<ShellDef> result;
|
||||
for (auto& sf : shell_files)
|
||||
result.insert(result.end(), sf.shells.begin(), sf.shells.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
void Project::save_config() {
|
||||
@@ -201,15 +200,6 @@ void Project::save_plans() {
|
||||
}
|
||||
}
|
||||
|
||||
void Project::save_shells() {
|
||||
if (shells.filepath.empty()) {
|
||||
report_status("Error: no shells loaded to save");
|
||||
return;
|
||||
}
|
||||
shells.save();
|
||||
report_status("Saved shells: " + shells.filepath.filename().string());
|
||||
}
|
||||
|
||||
void Project::open_config(const fs::path& new_config_path) {
|
||||
config_path = fs::canonical(new_config_path);
|
||||
config = RexConfig::load(config_path);
|
||||
@@ -217,16 +207,7 @@ void Project::open_config(const fs::path& new_config_path) {
|
||||
resolver.scan_and_populate(config.all_string_values());
|
||||
|
||||
// reload shells
|
||||
shells = ShellsFile();
|
||||
auto sp = resolved_shells_path();
|
||||
if (!sp.empty() && fs::exists(sp)) {
|
||||
try {
|
||||
shells = ShellsFile::load(sp);
|
||||
report_status("Loaded shells: " + sp.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
report_status(std::string("Error loading shells: ") + e.what());
|
||||
}
|
||||
}
|
||||
reload_shells();
|
||||
|
||||
// reload units
|
||||
load_all_units();
|
||||
@@ -241,7 +222,7 @@ void Project::close_config() {
|
||||
resolver = VarResolver();
|
||||
plans.clear();
|
||||
unit_files.clear();
|
||||
shells = ShellsFile();
|
||||
shell_files.clear();
|
||||
report_status("Config closed");
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
|
||||
std::vector<Plan> plans;
|
||||
std::vector<UnitFile> unit_files;
|
||||
ShellsFile shells;
|
||||
std::vector<ShellsFile> shell_files;
|
||||
|
||||
// status reporting
|
||||
using StatusCallback = void(*)(const std::string& msg, void* data);
|
||||
@@ -55,11 +55,10 @@ public:
|
||||
bool is_unit_name_taken(const std::string& name, const Unit* exclude = nullptr) const;
|
||||
void load_all_units();
|
||||
void load_plan(const std::filesystem::path& plan_path);
|
||||
void load_shells(const std::filesystem::path& shells_path);
|
||||
void reload_shells();
|
||||
std::vector<ShellDef> all_shells() const;
|
||||
void save_config();
|
||||
void save_plans();
|
||||
void save_shells();
|
||||
|
||||
void open_config(const std::filesystem::path& new_config_path);
|
||||
void close_config();
|
||||
|
||||
@@ -49,6 +49,7 @@ struct DialogState {
|
||||
GCancellable* cancellable = nullptr;
|
||||
|
||||
// widgets
|
||||
GtkWidget* entry_name = nullptr;
|
||||
GtkWidget* entry_target = nullptr;
|
||||
GtkWidget* box_target = nullptr;
|
||||
GtkWidget* switch_shell_cmd = nullptr;
|
||||
@@ -59,6 +60,7 @@ struct DialogState {
|
||||
GtkWidget* box_workdir = nullptr;
|
||||
GtkWidget* switch_rectify = nullptr;
|
||||
GtkWidget* entry_rectifier = nullptr;
|
||||
GtkWidget* box_rectifier = nullptr;
|
||||
GtkWidget* switch_active = nullptr;
|
||||
GtkWidget* switch_required = nullptr;
|
||||
GtkWidget* switch_set_user_ctx = nullptr;
|
||||
@@ -92,13 +94,15 @@ struct DialogState {
|
||||
|
||||
static void update_sensitivity(DialogState* s);
|
||||
|
||||
static void dlg_switch_toggled_cb(GObject* sw, GParamSpec*, gpointer data) {
|
||||
static gboolean dlg_switch_state_set_cb(GtkSwitch* sw, gboolean new_state, gpointer data) {
|
||||
auto* b = static_cast<DlgSwitchBinding*>(data);
|
||||
gtk_switch_set_state(sw, new_state);
|
||||
if (b->state) {
|
||||
*b->target = gtk_switch_get_active(GTK_SWITCH(sw));
|
||||
*b->target = new_state;
|
||||
if (!b->state->loading)
|
||||
update_sensitivity(b->state);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static int shell_index(const std::vector<ShellDef>& shells, const std::string& name) {
|
||||
@@ -110,6 +114,7 @@ static int shell_index(const std::vector<ShellDef>& shells, const std::string& n
|
||||
static GtkWidget* make_switch_row(GtkWidget* grid, int row, const char* label_text, GtkWidget** label_out) {
|
||||
auto* label = gtk_label_new(label_text);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 1.0f);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
|
||||
if (label_out) *label_out = label;
|
||||
auto* sw = gtk_switch_new();
|
||||
@@ -121,6 +126,7 @@ static GtkWidget* make_switch_row(GtkWidget* grid, int row, const char* label_te
|
||||
static GtkWidget* make_entry_row(GtkWidget* grid, int row, const char* label_text, GtkWidget** label_out) {
|
||||
auto* label = gtk_label_new(label_text);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 1.0f);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
|
||||
if (label_out) *label_out = label;
|
||||
auto* entry = gtk_entry_new();
|
||||
@@ -133,6 +139,7 @@ static GtkWidget* make_browse_row(DialogState* s, GtkWidget* grid, int row, cons
|
||||
GtkWidget** label_out, GtkWidget** box_out) {
|
||||
auto* label = gtk_label_new(label_text);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 1.0f);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
|
||||
if (label_out) *label_out = label;
|
||||
|
||||
@@ -143,7 +150,7 @@ static GtkWidget* make_browse_row(DialogState* s, GtkWidget* grid, int row, cons
|
||||
gtk_widget_set_hexpand(entry, TRUE);
|
||||
gtk_box_append(GTK_BOX(hbox), entry);
|
||||
|
||||
auto* btn = gtk_button_new_with_label("Select");
|
||||
auto* btn = gtk_button_new_with_label("Select...");
|
||||
gtk_box_append(GTK_BOX(hbox), btn);
|
||||
|
||||
struct BrowseBtnData {
|
||||
@@ -195,6 +202,7 @@ static GtkWidget* make_file_row(DialogState* s, GtkWidget* grid, int row, const
|
||||
GtkWidget** label_out, GtkWidget** box_out) {
|
||||
auto* label = gtk_label_new(label_text);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 1.0f);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);
|
||||
if (label_out) *label_out = label;
|
||||
|
||||
@@ -205,12 +213,15 @@ static GtkWidget* make_file_row(DialogState* s, GtkWidget* grid, int row, const
|
||||
gtk_widget_set_hexpand(entry, TRUE);
|
||||
gtk_box_append(GTK_BOX(hbox), entry);
|
||||
|
||||
auto* file_btn_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(file_btn_group, "linked");
|
||||
auto* btn_browse = gtk_button_new_with_label("Select");
|
||||
auto* btn_open = gtk_button_new_with_label("Open");
|
||||
auto* btn_new = gtk_button_new_with_label("Create");
|
||||
gtk_box_append(GTK_BOX(hbox), btn_browse);
|
||||
gtk_box_append(GTK_BOX(hbox), btn_open);
|
||||
gtk_box_append(GTK_BOX(hbox), btn_new);
|
||||
gtk_box_append(GTK_BOX(file_btn_group), btn_browse);
|
||||
gtk_box_append(GTK_BOX(file_btn_group), btn_open);
|
||||
gtk_box_append(GTK_BOX(file_btn_group), btn_new);
|
||||
gtk_box_append(GTK_BOX(hbox), file_btn_group);
|
||||
|
||||
struct FileBtnData {
|
||||
DialogState* state;
|
||||
@@ -345,7 +356,7 @@ static void update_sensitivity(DialogState* s) {
|
||||
|
||||
show(active && s->working_copy.is_shell_command, {s->label_shell_def, s->dropdown_shell_def, s->label_force_pty, s->switch_force_pty});
|
||||
show(active && s->working_copy.set_working_directory, {s->label_workdir, s->box_workdir});
|
||||
show(active && s->working_copy.rectify, {s->label_rectifier, s->entry_rectifier});
|
||||
show(active && s->working_copy.rectify, {s->label_rectifier, s->box_rectifier});
|
||||
show(active && s->working_copy.set_user_context, {s->label_user, s->entry_user, s->label_group, s->entry_group});
|
||||
show(active && s->working_copy.supply_environment, {s->label_environment, s->box_environment});
|
||||
}
|
||||
@@ -361,6 +372,7 @@ static void populate_and_connect(DialogState* s) {
|
||||
g_object_unref(string_list);
|
||||
|
||||
s->loading = true;
|
||||
gtk_editable_set_text(GTK_EDITABLE(s->entry_name), u.name.c_str());
|
||||
gtk_editable_set_text(GTK_EDITABLE(s->entry_target), u.target.c_str());
|
||||
gtk_switch_set_active(GTK_SWITCH(s->switch_shell_cmd), u.is_shell_command);
|
||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(s->dropdown_shell_def), shell_index(*s->shells, u.shell_definition));
|
||||
@@ -388,6 +400,7 @@ static void populate_and_connect(DialogState* s) {
|
||||
*eb->target = gtk_editable_get_text(e);
|
||||
}), eb);
|
||||
};
|
||||
bind_entry(s->entry_name, &u.name);
|
||||
bind_entry(s->entry_target, &u.target);
|
||||
bind_entry(s->entry_workdir, &u.working_directory);
|
||||
bind_entry(s->entry_rectifier, &u.rectifier);
|
||||
@@ -399,7 +412,7 @@ static void populate_and_connect(DialogState* s) {
|
||||
auto bind_switch = [s](GtkWidget* sw, bool* target) {
|
||||
auto* sb = new DlgSwitchBinding{s, target};
|
||||
s->switch_bindings.push_back(sb);
|
||||
g_signal_connect(sw, "notify::active", G_CALLBACK(dlg_switch_toggled_cb), sb);
|
||||
g_signal_connect(sw, "state-set", G_CALLBACK(dlg_switch_state_set_cb), sb);
|
||||
};
|
||||
bind_switch(s->switch_shell_cmd, &u.is_shell_command);
|
||||
bind_switch(s->switch_force_pty, &u.force_pty);
|
||||
@@ -428,29 +441,24 @@ UnitDialogResult show_unit_properties_dialog(GtkWindow* parent,
|
||||
gtk_window_set_title(GTK_WINDOW(win), ("Unit Properties: " + unit->name).c_str());
|
||||
gtk_window_set_transient_for(GTK_WINDOW(win), parent);
|
||||
gtk_window_set_modal(GTK_WINDOW(win), TRUE);
|
||||
gtk_window_set_default_size(GTK_WINDOW(win), 600, 550);
|
||||
gtk_window_set_default_size(GTK_WINDOW(win), 650, 620);
|
||||
|
||||
auto* outer_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_margin_start(outer_box, 16);
|
||||
gtk_widget_set_margin_end(outer_box, 16);
|
||||
gtk_widget_set_margin_start(outer_box, 20);
|
||||
gtk_widget_set_margin_end(outer_box, 20);
|
||||
gtk_widget_set_margin_top(outer_box, 16);
|
||||
gtk_widget_set_margin_bottom(outer_box, 16);
|
||||
|
||||
// Unit name header
|
||||
auto* name_header = gtk_label_new(nullptr);
|
||||
auto header_markup = std::string("<big><b>") + unit->name + "</b></big>";
|
||||
gtk_label_set_markup(GTK_LABEL(name_header), header_markup.c_str());
|
||||
gtk_widget_set_halign(name_header, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(outer_box), name_header);
|
||||
|
||||
// Scrolled content area
|
||||
auto* scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
|
||||
auto* grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
auto* content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_set_margin_start(content, 4);
|
||||
gtk_widget_set_margin_end(content, 4);
|
||||
gtk_widget_set_margin_top(content, 8);
|
||||
gtk_widget_set_margin_bottom(content, 8);
|
||||
|
||||
auto* loop = g_main_loop_new(nullptr, FALSE);
|
||||
auto* cancellable = g_cancellable_new();
|
||||
@@ -464,38 +472,93 @@ UnitDialogResult show_unit_properties_dialog(GtkWindow* parent,
|
||||
state.shells = &shells;
|
||||
state.cancellable = cancellable;
|
||||
|
||||
int r = 0;
|
||||
state.entry_target = make_file_row(&state, grid, r++, "Target", &state.label_target, &state.box_target);
|
||||
state.switch_shell_cmd = make_switch_row(grid, r++, "Shell Command", &state.label_shell_cmd);
|
||||
// Helper to create a framed section with a bold title label
|
||||
auto make_section = [&](const char* title) {
|
||||
auto* frame = gtk_frame_new(nullptr);
|
||||
auto* frame_label = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(frame_label), (std::string(" <b>") + title + "</b> ").c_str());
|
||||
gtk_frame_set_label_widget(GTK_FRAME(frame), frame_label);
|
||||
gtk_box_append(GTK_BOX(content), frame);
|
||||
return frame;
|
||||
};
|
||||
|
||||
// Helper to create a grid inside a frame with internal padding
|
||||
auto make_grid = [&](GtkWidget* frame) {
|
||||
auto* grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
gtk_widget_set_margin_start(grid, 12);
|
||||
gtk_widget_set_margin_end(grid, 12);
|
||||
gtk_widget_set_margin_top(grid, 8);
|
||||
gtk_widget_set_margin_bottom(grid, 12);
|
||||
gtk_frame_set_child(GTK_FRAME(frame), grid);
|
||||
return grid;
|
||||
};
|
||||
|
||||
// === Identity ===
|
||||
auto* id_frame = make_section("Identity");
|
||||
auto* id_grid = make_grid(id_frame);
|
||||
state.entry_name = make_entry_row(id_grid, 0, "Name", nullptr);
|
||||
|
||||
// === Status ===
|
||||
auto* status_frame = make_section("Status");
|
||||
auto* status_grid = make_grid(status_frame);
|
||||
state.switch_active = make_switch_row(status_grid, 0, "Active", &state.label_active);
|
||||
state.switch_required = make_switch_row(status_grid, 1, "Required", &state.label_required);
|
||||
|
||||
// === Execution ===
|
||||
auto* exec_frame = make_section("Execution");
|
||||
auto* exec_grid = make_grid(exec_frame);
|
||||
int r = 0;
|
||||
state.entry_target = make_file_row(&state, exec_grid, r++, "Target", &state.label_target, &state.box_target);
|
||||
state.switch_shell_cmd = make_switch_row(exec_grid, r++, "Shell Command", &state.label_shell_cmd);
|
||||
|
||||
// Shell definition dropdown
|
||||
state.label_shell_def = gtk_label_new("Shell Definition");
|
||||
gtk_label_set_xalign(GTK_LABEL(state.label_shell_def), 1.0f);
|
||||
gtk_grid_attach(GTK_GRID(grid), state.label_shell_def, 0, r, 1, 1);
|
||||
gtk_widget_add_css_class(state.label_shell_def, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(exec_grid), state.label_shell_def, 0, r, 1, 1);
|
||||
auto* string_list = gtk_string_list_new(nullptr);
|
||||
state.dropdown_shell_def = gtk_drop_down_new(G_LIST_MODEL(string_list), nullptr);
|
||||
gtk_widget_set_hexpand(state.dropdown_shell_def, TRUE);
|
||||
gtk_grid_attach(GTK_GRID(grid), state.dropdown_shell_def, 1, r++, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(exec_grid), state.dropdown_shell_def, 1, r++, 1, 1);
|
||||
|
||||
state.switch_force_pty = make_switch_row(grid, r++, "Force PTY", &state.label_force_pty);
|
||||
state.switch_set_workdir = make_switch_row(grid, r++, "Set Working Dir", &state.label_set_workdir);
|
||||
state.entry_workdir = make_browse_row(&state, grid, r++, "Working Directory", &state.label_workdir, &state.box_workdir);
|
||||
state.switch_rectify = make_switch_row(grid, r++, "Rectify", &state.label_rectify);
|
||||
state.entry_rectifier = make_entry_row(grid, r++, "Rectifier", &state.label_rectifier);
|
||||
state.switch_active = make_switch_row(grid, r++, "Active", &state.label_active);
|
||||
state.switch_required = make_switch_row(grid, r++, "Required", &state.label_required);
|
||||
state.switch_set_user_ctx = make_switch_row(grid, r++, "Set User Context", &state.label_set_user_ctx);
|
||||
state.entry_user = make_entry_row(grid, r++, "User", &state.label_user);
|
||||
state.entry_group = make_entry_row(grid, r++, "Group", &state.label_group);
|
||||
state.switch_supply_env = make_switch_row(grid, r++, "Supply Environment", &state.label_supply_env);
|
||||
state.entry_environment = make_file_row(&state, grid, r++, "Environment", &state.label_environment, &state.box_environment);
|
||||
state.switch_force_pty = make_switch_row(exec_grid, r++, "Force PTY", &state.label_force_pty);
|
||||
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), grid);
|
||||
// === Working Directory ===
|
||||
auto* wd_frame = make_section("Working Directory");
|
||||
auto* wd_grid = make_grid(wd_frame);
|
||||
state.switch_set_workdir = make_switch_row(wd_grid, 0, "Set Working Dir", &state.label_set_workdir);
|
||||
state.entry_workdir = make_browse_row(&state, wd_grid, 1, "Working Directory", &state.label_workdir, &state.box_workdir);
|
||||
|
||||
// === Validation ===
|
||||
auto* val_frame = make_section("Validation");
|
||||
auto* val_grid = make_grid(val_frame);
|
||||
state.switch_rectify = make_switch_row(val_grid, 0, "Rectify", &state.label_rectify);
|
||||
state.entry_rectifier = make_file_row(&state, val_grid, 1, "Rectifier", &state.label_rectifier, &state.box_rectifier);
|
||||
|
||||
// === Security ===
|
||||
auto* sec_frame = make_section("Security");
|
||||
auto* sec_grid = make_grid(sec_frame);
|
||||
state.switch_set_user_ctx = make_switch_row(sec_grid, 0, "Set User Context", &state.label_set_user_ctx);
|
||||
state.entry_user = make_entry_row(sec_grid, 1, "User", &state.label_user);
|
||||
state.entry_group = make_entry_row(sec_grid, 2, "Group", &state.label_group);
|
||||
|
||||
// === Environment ===
|
||||
auto* env_frame = make_section("Environment");
|
||||
auto* env_grid = make_grid(env_frame);
|
||||
state.switch_supply_env = make_switch_row(env_grid, 0, "Supply Environment", &state.label_supply_env);
|
||||
state.entry_environment = make_file_row(&state, env_grid, 1, "Environment", &state.label_environment, &state.box_environment);
|
||||
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), content);
|
||||
gtk_box_append(GTK_BOX(outer_box), scroll);
|
||||
|
||||
// Button row
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
auto* btn_sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_widget_set_margin_top(btn_sep, 4);
|
||||
gtk_box_append(GTK_BOX(outer_box), btn_sep);
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(btn_row, GTK_ALIGN_END);
|
||||
gtk_widget_set_margin_top(btn_row, 4);
|
||||
auto* btn_cancel = gtk_button_new_with_label("Cancel");
|
||||
auto* btn_save = gtk_button_new_with_label("Save");
|
||||
gtk_widget_add_css_class(btn_save, "suggested-action");
|
||||
@@ -518,6 +581,15 @@ UnitDialogResult show_unit_properties_dialog(GtkWindow* parent,
|
||||
|
||||
g_signal_connect(btn_save, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
auto* s = static_cast<DialogState*>(d);
|
||||
if (s->working_copy.name.empty()) {
|
||||
s->project->report_status("Error: unit name cannot be empty");
|
||||
return;
|
||||
}
|
||||
if (s->working_copy.name != s->original->name &&
|
||||
s->project->is_unit_name_taken(s->working_copy.name, s->original)) {
|
||||
s->project->report_status("Error: unit '" + s->working_copy.name + "' already exists");
|
||||
return;
|
||||
}
|
||||
*s->original = s->working_copy;
|
||||
s->result = UnitDialogResult::Save;
|
||||
gtk_window_close(GTK_WINDOW(s->window));
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "util/unsaved_dialog.h"
|
||||
#include <string>
|
||||
|
||||
namespace grex {
|
||||
|
||||
@@ -26,46 +27,58 @@ struct DialogState {
|
||||
GtkWidget* window;
|
||||
};
|
||||
|
||||
static void on_revert_clicked(GtkButton*, gpointer d) {
|
||||
auto* state = static_cast<DialogState*>(d);
|
||||
state->result = UnsavedResult::Revert;
|
||||
gtk_window_close(GTK_WINDOW(state->window));
|
||||
// When the parent gets WM focus while our dialog is up, yank it back.
|
||||
static void on_parent_activated(GObject*, GParamSpec*, gpointer d) {
|
||||
auto* dialog_win = static_cast<GtkWidget*>(d);
|
||||
gtk_window_present(GTK_WINDOW(dialog_win));
|
||||
}
|
||||
|
||||
static void on_save_clicked(GtkButton*, gpointer d) {
|
||||
auto* state = static_cast<DialogState*>(d);
|
||||
state->result = UnsavedResult::Save;
|
||||
gtk_window_close(GTK_WINDOW(state->window));
|
||||
}
|
||||
|
||||
static void on_dialog_closed(GtkWidget*, gpointer d) {
|
||||
auto* state = static_cast<DialogState*>(d);
|
||||
g_main_loop_quit(state->loop);
|
||||
}
|
||||
static bool dialog_active = false;
|
||||
|
||||
UnsavedResult show_unsaved_dialog(GtkWindow* parent) {
|
||||
if (dialog_active)
|
||||
return UnsavedResult::Cancel;
|
||||
dialog_active = true;
|
||||
|
||||
auto* win = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(win), "Unsaved Changes");
|
||||
gtk_window_set_transient_for(GTK_WINDOW(win), parent);
|
||||
gtk_window_set_modal(GTK_WINDOW(win), TRUE);
|
||||
gtk_window_set_default_size(GTK_WINDOW(win), 350, -1);
|
||||
gtk_window_set_default_size(GTK_WINDOW(win), 380, -1);
|
||||
gtk_window_set_resizable(GTK_WINDOW(win), FALSE);
|
||||
|
||||
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);
|
||||
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
|
||||
gtk_widget_set_margin_start(box, 24);
|
||||
gtk_widget_set_margin_end(box, 24);
|
||||
gtk_widget_set_margin_top(box, 20);
|
||||
gtk_widget_set_margin_bottom(box, 20);
|
||||
|
||||
auto* label = gtk_label_new("You have unsaved changes.");
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
// Header
|
||||
auto* header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(header), "<big><b>Unsaved Changes</b></big>");
|
||||
gtk_widget_set_halign(header, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(box), header);
|
||||
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
// Description
|
||||
auto* desc = gtk_label_new("You have unsaved changes that will be lost if you continue without saving.");
|
||||
gtk_label_set_wrap(GTK_LABEL(desc), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(desc), 0.5f);
|
||||
gtk_widget_add_css_class(desc, "dim-label");
|
||||
gtk_box_append(GTK_BOX(box), desc);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// Button row
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(btn_row, GTK_ALIGN_END);
|
||||
auto* btn_revert = gtk_button_new_with_label("Revert");
|
||||
|
||||
auto* btn_discard = gtk_button_new_with_label("Discard");
|
||||
auto* btn_cancel = gtk_button_new_with_label("Cancel");
|
||||
auto* btn_save = gtk_button_new_with_label("Save");
|
||||
gtk_widget_add_css_class(btn_discard, "destructive-action");
|
||||
gtk_widget_add_css_class(btn_save, "suggested-action");
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_revert);
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_discard);
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_cancel);
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_save);
|
||||
gtk_box_append(GTK_BOX(box), btn_row);
|
||||
|
||||
@@ -73,15 +86,35 @@ UnsavedResult show_unsaved_dialog(GtkWindow* parent) {
|
||||
gtk_window_set_default_widget(GTK_WINDOW(win), btn_save);
|
||||
|
||||
auto* loop = g_main_loop_new(nullptr, FALSE);
|
||||
DialogState state{UnsavedResult::Save, loop, win};
|
||||
DialogState state{UnsavedResult::Cancel, loop, win};
|
||||
|
||||
g_signal_connect(btn_revert, "clicked", G_CALLBACK(on_revert_clicked), &state);
|
||||
g_signal_connect(btn_save, "clicked", G_CALLBACK(on_save_clicked), &state);
|
||||
g_signal_connect(win, "destroy", G_CALLBACK(on_dialog_closed), &state);
|
||||
auto on_btn = +[](GtkButton* btn, gpointer d) {
|
||||
auto* s = static_cast<DialogState*>(d);
|
||||
auto label = std::string(gtk_button_get_label(btn));
|
||||
if (label == "Save") s->result = UnsavedResult::Save;
|
||||
else if (label == "Discard") s->result = UnsavedResult::Discard;
|
||||
else s->result = UnsavedResult::Cancel;
|
||||
gtk_window_close(GTK_WINDOW(s->window));
|
||||
};
|
||||
|
||||
g_signal_connect(btn_discard, "clicked", G_CALLBACK(on_btn), &state);
|
||||
g_signal_connect(btn_cancel, "clicked", G_CALLBACK(on_btn), &state);
|
||||
g_signal_connect(btn_save, "clicked", G_CALLBACK(on_btn), &state);
|
||||
g_signal_connect(win, "destroy", G_CALLBACK(+[](GtkWidget*, gpointer d) {
|
||||
auto* s = static_cast<DialogState*>(d);
|
||||
g_main_loop_quit(s->loop);
|
||||
}), &state);
|
||||
|
||||
// Steal focus back if WM raises the parent while dialog is up
|
||||
auto focus_guard = g_signal_connect(parent, "notify::is-active",
|
||||
G_CALLBACK(on_parent_activated), win);
|
||||
|
||||
gtk_window_present(GTK_WINDOW(win));
|
||||
g_main_loop_run(loop);
|
||||
|
||||
g_signal_handler_disconnect(parent, focus_guard);
|
||||
g_main_loop_unref(loop);
|
||||
dialog_active = false;
|
||||
|
||||
return state.result;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
namespace grex {
|
||||
|
||||
enum class UnsavedResult { Save, Revert };
|
||||
enum class UnsavedResult { Save, Discard, Cancel };
|
||||
|
||||
// Shows a blocking modal "Unsaved Changes" dialog with Revert and Save buttons.
|
||||
// Shows a blocking modal "Unsaved Changes" dialog with Discard, Cancel, and Save buttons.
|
||||
// Returns the user's choice. Uses a nested GLib main loop to block.
|
||||
UnsavedResult show_unsaved_dialog(GtkWindow* parent);
|
||||
|
||||
|
||||
@@ -48,13 +48,13 @@ ConfigView::ConfigView(Project& project) : project_(project) {
|
||||
// === Config file label + Open/Close buttons ===
|
||||
config_label_ = gtk_label_new(nullptr);
|
||||
gtk_label_set_xalign(GTK_LABEL(config_label_), 0.0f);
|
||||
gtk_widget_add_css_class(config_label_, "title-3");
|
||||
gtk_box_append(GTK_BOX(box), config_label_);
|
||||
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
btn_open_ = gtk_button_new_with_label("Open Config");
|
||||
btn_create_ = gtk_button_new_with_label("Create Config");
|
||||
btn_close_ = gtk_button_new_with_label("Close Config");
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(btn_row, "linked");
|
||||
btn_open_ = gtk_button_new_with_label("Open");
|
||||
btn_create_ = gtk_button_new_with_label("Create");
|
||||
btn_close_ = gtk_button_new_with_label("Close");
|
||||
gtk_widget_set_hexpand(btn_open_, TRUE);
|
||||
gtk_widget_set_hexpand(btn_create_, TRUE);
|
||||
gtk_widget_set_hexpand(btn_close_, TRUE);
|
||||
@@ -67,67 +67,72 @@ ConfigView::ConfigView(Project& project) : project_(project) {
|
||||
g_signal_connect(btn_create_, "clicked", G_CALLBACK(on_create_config), this);
|
||||
g_signal_connect(btn_close_, "clicked", G_CALLBACK(on_close_config), this);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
// === 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(box), config_header);
|
||||
gtk_box_append(GTK_BOX(config_content_), config_header);
|
||||
|
||||
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(box), config_grid_);
|
||||
gtk_box_append(GTK_BOX(config_content_), config_grid_);
|
||||
|
||||
build_config_fields();
|
||||
|
||||
// === Resolved paths section ===
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
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(box), resolved_header);
|
||||
gtk_box_append(GTK_BOX(config_content_), resolved_header);
|
||||
|
||||
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(box), resolved_grid_);
|
||||
gtk_box_append(GTK_BOX(config_content_), resolved_grid_);
|
||||
|
||||
// === Variables section ===
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
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(box), vars_header);
|
||||
gtk_box_append(GTK_BOX(config_content_), vars_header);
|
||||
|
||||
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_box_append(GTK_BOX(box), vars_desc);
|
||||
gtk_widget_add_css_class(vars_desc, "dim-label");
|
||||
gtk_box_append(GTK_BOX(config_content_), 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(box), vars_grid_);
|
||||
gtk_box_append(GTK_BOX(config_content_), vars_grid_);
|
||||
|
||||
build_variables_section();
|
||||
update_resolved_labels();
|
||||
|
||||
// === Save button ===
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_box_append(GTK_BOX(config_content_), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
btn_save_ = gtk_button_new_with_label("Save Config");
|
||||
gtk_widget_set_halign(btn_save_, GTK_ALIGN_END);
|
||||
gtk_box_append(GTK_BOX(box), btn_save_);
|
||||
gtk_box_append(GTK_BOX(config_content_), btn_save_);
|
||||
|
||||
g_signal_connect(btn_save_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
static_cast<ConfigView*>(d)->apply_config();
|
||||
}), this);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), config_content_);
|
||||
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), box);
|
||||
|
||||
update_config_buttons();
|
||||
@@ -233,8 +238,6 @@ void ConfigView::build_variables_section() {
|
||||
if (b->view->rebuilding_) return;
|
||||
b->view->project_.resolver.set(b->var_name, gtk_editable_get_text(e));
|
||||
b->view->update_resolved_labels();
|
||||
b->view->dirty_ = true;
|
||||
gtk_widget_add_css_class(b->view->btn_save_, "suggested-action");
|
||||
}), binding);
|
||||
|
||||
row++;
|
||||
@@ -322,13 +325,7 @@ void ConfigView::apply_config() {
|
||||
project_.report_status("Config saved: " + project_.config_path.filename().string());
|
||||
|
||||
// reload shells if path now resolves
|
||||
auto sp = project_.resolved_shells_path();
|
||||
if (!sp.empty() && fs::exists(sp)) {
|
||||
project_.shells = ShellsFile::load(sp);
|
||||
project_.report_status("Loaded shells: " + sp.filename().string());
|
||||
} else if (!sp.empty()) {
|
||||
project_.report_status("Error: shells path not found: " + sp.string());
|
||||
}
|
||||
project_.reload_shells();
|
||||
|
||||
// notify MainWindow to refresh other views
|
||||
if (apply_cb_)
|
||||
@@ -349,6 +346,7 @@ void ConfigView::update_config_buttons() {
|
||||
gtk_widget_set_visible(btn_open_, !has_config);
|
||||
gtk_widget_set_visible(btn_create_, !has_config);
|
||||
gtk_widget_set_visible(btn_close_, has_config);
|
||||
gtk_widget_set_sensitive(config_content_, has_config);
|
||||
}
|
||||
|
||||
void ConfigView::refresh() {
|
||||
@@ -466,6 +464,8 @@ void ConfigView::on_close_config(GtkButton*, gpointer data) {
|
||||
if (self->dirty_) {
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unsaved_dialog(parent);
|
||||
if (result == UnsavedResult::Cancel)
|
||||
return;
|
||||
if (result == UnsavedResult::Save)
|
||||
self->apply_config();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ private:
|
||||
GtkWidget* btn_create_;
|
||||
GtkWidget* btn_close_;
|
||||
GtkWidget* btn_save_;
|
||||
GtkWidget* config_content_;
|
||||
|
||||
void build_config_fields();
|
||||
void build_variables_section();
|
||||
|
||||
@@ -25,60 +25,95 @@
|
||||
|
||||
namespace grex {
|
||||
|
||||
struct TabClickData {
|
||||
MainWindow* self;
|
||||
int index;
|
||||
};
|
||||
|
||||
MainWindow::MainWindow(GtkApplication* app, Project& project, GrexConfig& grex_config)
|
||||
: project_(project), grex_config_(grex_config) {
|
||||
window_ = gtk_application_window_new(app);
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "grex");
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "GREX: Rex Config");
|
||||
gtk_window_set_default_size(GTK_WINDOW(window_), 1200, 800);
|
||||
|
||||
auto* vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
// Header bar
|
||||
auto* header = gtk_header_bar_new();
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "GREX: Rex Config");
|
||||
gtk_window_set_titlebar(GTK_WINDOW(window_), header);
|
||||
|
||||
// Grex Config button in header bar (opens modal dialog)
|
||||
// Grex Config button in header bar
|
||||
auto* grex_btn = gtk_button_new_with_label("Grex Config");
|
||||
gtk_widget_remove_css_class(grex_btn, "flat");
|
||||
gtk_header_bar_pack_end(GTK_HEADER_BAR(header), grex_btn);
|
||||
g_signal_connect(grex_btn, "clicked", G_CALLBACK(on_grex_config_clicked), this);
|
||||
|
||||
// Stack + switcher
|
||||
// Stack (no GtkStackSwitcher — we use our own buttons)
|
||||
stack_ = gtk_stack_new();
|
||||
auto* stack = stack_;
|
||||
gtk_stack_set_transition_type(GTK_STACK(stack), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
||||
|
||||
switcher_ = gtk_stack_switcher_new();
|
||||
gtk_stack_switcher_set_stack(GTK_STACK_SWITCHER(switcher_), GTK_STACK(stack));
|
||||
gtk_widget_set_halign(switcher_, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(vbox), switcher_);
|
||||
gtk_stack_set_transition_type(GTK_STACK(stack_), GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
||||
|
||||
// Wire project status reporting to status bar
|
||||
project_.status_cb = on_project_status;
|
||||
project_.status_cb_data = this;
|
||||
|
||||
// Rex Config view
|
||||
// Create views
|
||||
config_view_ = new ConfigView(project_);
|
||||
config_view_->set_apply_callback(on_config_applied, this);
|
||||
gtk_stack_add_titled(GTK_STACK(stack), config_view_->widget(), "config", "Rex Config");
|
||||
gtk_stack_add_named(GTK_STACK(stack_), config_view_->widget(), "config");
|
||||
|
||||
// Plan view
|
||||
plan_view_ = new PlanView(project_, grex_config_);
|
||||
gtk_stack_add_titled(GTK_STACK(stack), plan_view_->widget(), "plans", "Plans");
|
||||
gtk_stack_add_named(GTK_STACK(stack_), plan_view_->widget(), "plans");
|
||||
|
||||
// Units view
|
||||
units_view_ = new UnitsView(project_, grex_config_);
|
||||
gtk_stack_add_titled(GTK_STACK(stack), units_view_->widget(), "units", "Units");
|
||||
gtk_stack_add_named(GTK_STACK(stack_), units_view_->widget(), "units");
|
||||
|
||||
// Shells view
|
||||
shells_view_ = new ShellsView(project_);
|
||||
gtk_stack_add_titled(GTK_STACK(stack), shells_view_->widget(), "shells", "Shells");
|
||||
gtk_stack_add_named(GTK_STACK(stack_), shells_view_->widget(), "shells");
|
||||
|
||||
gtk_widget_set_vexpand(stack, TRUE);
|
||||
gtk_box_append(GTK_BOX(vbox), stack);
|
||||
// Tab bar — plain buttons, we control switching
|
||||
auto* tab_bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_set_halign(tab_bar, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(tab_bar, "linked");
|
||||
|
||||
g_signal_connect(stack, "notify::visible-child", G_CALLBACK(on_stack_page_changed), this);
|
||||
const char* tab_labels[] = {"Rex Config", "Units", "Plans", "Shells"};
|
||||
GtkWidget* tab_pages[] = {
|
||||
config_view_->widget(), units_view_->widget(),
|
||||
plan_view_->widget(), shells_view_->widget()
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
auto* btn = gtk_toggle_button_new_with_label(tab_labels[i]);
|
||||
gtk_box_append(GTK_BOX(tab_bar), btn);
|
||||
tabs_.push_back({tab_labels[i], tab_pages[i], btn});
|
||||
|
||||
auto* data = new TabClickData{this, i};
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(+[](GtkToggleButton* btn, gpointer d) {
|
||||
auto* td = static_cast<TabClickData*>(d);
|
||||
auto* self = td->self;
|
||||
int idx = td->index;
|
||||
|
||||
// If clicking the already-active tab, keep it toggled and do nothing
|
||||
if (idx == self->current_tab_) {
|
||||
gtk_toggle_button_set_active(btn, TRUE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check dirty state before allowing switch
|
||||
if (!self->check_dirty_and_resolve()) {
|
||||
// Cancelled — re-toggle current tab button, untoggle this one
|
||||
self->update_tab_buttons();
|
||||
return;
|
||||
}
|
||||
|
||||
self->switch_to_tab(idx);
|
||||
}), data);
|
||||
}
|
||||
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tabs_[0].button), TRUE);
|
||||
|
||||
gtk_box_append(GTK_BOX(vbox), tab_bar);
|
||||
gtk_widget_set_vexpand(stack_, TRUE);
|
||||
gtk_box_append(GTK_BOX(vbox), stack_);
|
||||
|
||||
// Status bar
|
||||
gtk_box_append(GTK_BOX(vbox), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
@@ -94,10 +129,7 @@ MainWindow::MainWindow(GtkApplication* app, Project& project, GrexConfig& grex_c
|
||||
gtk_box_append(GTK_BOX(status_bar), status_label_);
|
||||
gtk_box_append(GTK_BOX(vbox), status_bar);
|
||||
|
||||
prev_page_ = config_view_->widget();
|
||||
|
||||
gtk_window_set_child(GTK_WINDOW(window_), vbox);
|
||||
|
||||
update_tab_sensitivity();
|
||||
}
|
||||
|
||||
@@ -112,6 +144,54 @@ void MainWindow::set_status(const std::string& msg) {
|
||||
gtk_label_set_text(GTK_LABEL(status_label_), msg.c_str());
|
||||
}
|
||||
|
||||
void MainWindow::update_tab_buttons() {
|
||||
for (int i = 0; i < (int)tabs_.size(); i++)
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tabs_[i].button), i == current_tab_);
|
||||
}
|
||||
|
||||
bool MainWindow::check_dirty_and_resolve() {
|
||||
auto* cur = tabs_[current_tab_].page;
|
||||
|
||||
bool dirty = false;
|
||||
if (cur == config_view_->widget() && config_view_->is_dirty())
|
||||
dirty = true;
|
||||
else if (cur == plan_view_->widget() && plan_view_->is_dirty())
|
||||
dirty = true;
|
||||
else if (cur == units_view_->widget() && units_view_->is_dirty())
|
||||
dirty = true;
|
||||
|
||||
if (!dirty)
|
||||
return true;
|
||||
|
||||
auto result = show_unsaved_dialog(GTK_WINDOW(window_));
|
||||
if (result == UnsavedResult::Cancel)
|
||||
return false;
|
||||
|
||||
if (result == UnsavedResult::Save) {
|
||||
if (cur == config_view_->widget())
|
||||
config_view_->apply_config();
|
||||
else if (cur == plan_view_->widget())
|
||||
plan_view_->save_dirty();
|
||||
else if (cur == units_view_->widget())
|
||||
units_view_->save_current_file();
|
||||
} else {
|
||||
if (cur == config_view_->widget())
|
||||
config_view_->refresh();
|
||||
else if (cur == plan_view_->widget())
|
||||
plan_view_->revert_dirty();
|
||||
else if (cur == units_view_->widget())
|
||||
units_view_->refresh();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::switch_to_tab(int idx) {
|
||||
current_tab_ = idx;
|
||||
gtk_stack_set_visible_child(GTK_STACK(stack_), tabs_[idx].page);
|
||||
update_tab_buttons();
|
||||
refresh_visible_page();
|
||||
}
|
||||
|
||||
void MainWindow::on_config_applied(void* data) {
|
||||
auto* self = static_cast<MainWindow*>(data);
|
||||
self->update_tab_sensitivity();
|
||||
@@ -123,82 +203,25 @@ void MainWindow::on_config_applied(void* data) {
|
||||
void MainWindow::update_tab_sensitivity() {
|
||||
bool has_config = !project_.config_path.empty();
|
||||
|
||||
// Enable/disable the page content
|
||||
gtk_widget_set_sensitive(plan_view_->widget(), has_config);
|
||||
gtk_widget_set_sensitive(units_view_->widget(), has_config);
|
||||
gtk_widget_set_sensitive(shells_view_->widget(), has_config);
|
||||
|
||||
// Grey out the switcher buttons for disabled tabs
|
||||
// GtkStackSwitcher children are toggle buttons in page order
|
||||
auto* child = gtk_widget_get_first_child(switcher_);
|
||||
int i = 0;
|
||||
while (child) {
|
||||
// child 0 = Rex Config (always enabled), 1-3 = Plans/Units/Shells
|
||||
if (i > 0)
|
||||
gtk_widget_set_sensitive(child, has_config);
|
||||
child = gtk_widget_get_next_sibling(child);
|
||||
i++;
|
||||
}
|
||||
// Disable tab buttons for non-config tabs when no config loaded
|
||||
for (int i = 1; i < (int)tabs_.size(); i++)
|
||||
gtk_widget_set_sensitive(tabs_[i].button, has_config);
|
||||
|
||||
// If config was just closed and we're on a non-config tab, switch to config
|
||||
if (!has_config) {
|
||||
auto* visible = gtk_stack_get_visible_child(GTK_STACK(stack_));
|
||||
if (visible != config_view_->widget())
|
||||
gtk_stack_set_visible_child(GTK_STACK(stack_), config_view_->widget());
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_stack_page_changed(GObject* stack, GParamSpec*, gpointer data) {
|
||||
auto* self = static_cast<MainWindow*>(data);
|
||||
auto* visible = gtk_stack_get_visible_child(GTK_STACK(stack));
|
||||
|
||||
// Check if previous page has unsaved changes
|
||||
auto* prev = self->prev_page_;
|
||||
self->prev_page_ = visible;
|
||||
|
||||
bool prev_dirty = false;
|
||||
if (prev == self->config_view_->widget() && self->config_view_->is_dirty())
|
||||
prev_dirty = true;
|
||||
else if (prev == self->plan_view_->widget() && self->plan_view_->is_dirty())
|
||||
prev_dirty = true;
|
||||
else if (prev == self->units_view_->widget() && self->units_view_->is_dirty())
|
||||
prev_dirty = true;
|
||||
|
||||
if (prev_dirty) {
|
||||
auto result = show_unsaved_dialog(GTK_WINDOW(self->window_));
|
||||
if (result == UnsavedResult::Save) {
|
||||
if (self->config_view_->is_dirty())
|
||||
self->config_view_->apply_config();
|
||||
if (self->plan_view_->is_dirty())
|
||||
self->plan_view_->save_dirty();
|
||||
if (self->units_view_->is_dirty())
|
||||
self->units_view_->save_current_file();
|
||||
} else {
|
||||
if (self->config_view_->is_dirty())
|
||||
self->config_view_->refresh();
|
||||
if (self->plan_view_->is_dirty())
|
||||
self->plan_view_->revert_dirty();
|
||||
if (self->units_view_->is_dirty())
|
||||
self->units_view_->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// No dirty state — refresh immediately
|
||||
self->refresh_visible_page();
|
||||
if (!has_config && current_tab_ != 0)
|
||||
switch_to_tab(0);
|
||||
}
|
||||
|
||||
void MainWindow::refresh_visible_page() {
|
||||
auto* visible = gtk_stack_get_visible_child(GTK_STACK(stack_));
|
||||
auto* visible = tabs_[current_tab_].page;
|
||||
|
||||
// Update window title for current tab
|
||||
if (visible == config_view_->widget())
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "GREX: Rex Config");
|
||||
else if (visible == plan_view_->widget())
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "GREX: Plans");
|
||||
else if (visible == units_view_->widget())
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "GREX: Units");
|
||||
else if (visible == shells_view_->widget())
|
||||
gtk_window_set_title(GTK_WINDOW(window_), "GREX: Shells");
|
||||
// Update window title
|
||||
auto title = std::string("GREX: ") + tabs_[current_tab_].name;
|
||||
gtk_window_set_title(GTK_WINDOW(window_), title.c_str());
|
||||
|
||||
// Refresh the newly visible page
|
||||
if (visible == plan_view_->widget()) {
|
||||
@@ -239,14 +262,28 @@ void MainWindow::on_grex_config_clicked(GtkButton*, gpointer data) {
|
||||
gtk_window_set_modal(GTK_WINDOW(win), TRUE);
|
||||
gtk_window_set_default_size(GTK_WINDOW(win), 400, -1);
|
||||
|
||||
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);
|
||||
auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
|
||||
gtk_widget_set_margin_start(box, 24);
|
||||
gtk_widget_set_margin_end(box, 24);
|
||||
gtk_widget_set_margin_top(box, 20);
|
||||
gtk_widget_set_margin_bottom(box, 20);
|
||||
|
||||
// Header
|
||||
auto* header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(header), "<big><b>GREX Preferences</b></big>");
|
||||
gtk_widget_set_halign(header, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(box), header);
|
||||
|
||||
auto* desc = gtk_label_new("Application settings stored in ~/.config/grex/grex.ini");
|
||||
gtk_label_set_xalign(GTK_LABEL(desc), 0.5f);
|
||||
gtk_widget_add_css_class(desc, "dim-label");
|
||||
gtk_box_append(GTK_BOX(box), desc);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// Settings grid
|
||||
auto* grid = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
|
||||
gtk_grid_set_row_spacing(GTK_GRID(grid), 10);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
|
||||
|
||||
auto* label = gtk_label_new("File Editor");
|
||||
@@ -255,20 +292,33 @@ void MainWindow::on_grex_config_clicked(GtkButton*, gpointer data) {
|
||||
|
||||
auto* entry = gtk_entry_new();
|
||||
gtk_widget_set_hexpand(entry, TRUE);
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "e.g. xdg-open, vim, code");
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry), self->grex_config_.file_editor.c_str());
|
||||
gtk_grid_attach(GTK_GRID(grid), entry, 1, 0, 1, 1);
|
||||
|
||||
auto* hint = gtk_label_new("Command used to open files from unit target/environment fields.");
|
||||
gtk_label_set_xalign(GTK_LABEL(hint), 0.0f);
|
||||
gtk_label_set_wrap(GTK_LABEL(hint), TRUE);
|
||||
gtk_widget_add_css_class(hint, "dim-label");
|
||||
gtk_grid_attach(GTK_GRID(grid), hint, 1, 1, 1, 1);
|
||||
|
||||
gtk_box_append(GTK_BOX(box), grid);
|
||||
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// Buttons
|
||||
auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_halign(btn_row, GTK_ALIGN_END);
|
||||
auto* btn_cancel = gtk_button_new_with_label("Cancel");
|
||||
auto* btn_save = gtk_button_new_with_label("Save");
|
||||
gtk_widget_add_css_class(btn_save, "suggested-action");
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_cancel);
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_save);
|
||||
gtk_box_append(GTK_BOX(box), btn_row);
|
||||
|
||||
gtk_window_set_child(GTK_WINDOW(win), box);
|
||||
gtk_window_set_default_widget(GTK_WINDOW(win), btn_save);
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
||||
|
||||
auto* dd = new GCDialogData{self, win, entry};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#pragma once
|
||||
#include <gtk/gtk.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "models/project.h"
|
||||
#include "models/grex_config.h"
|
||||
|
||||
@@ -45,15 +46,23 @@ private:
|
||||
ShellsView* shells_view_;
|
||||
|
||||
GtkWidget* stack_;
|
||||
GtkWidget* switcher_;
|
||||
GtkWidget* status_label_;
|
||||
GtkWidget* prev_page_ = nullptr;
|
||||
|
||||
struct TabInfo {
|
||||
const char* name;
|
||||
GtkWidget* page;
|
||||
GtkWidget* button;
|
||||
};
|
||||
std::vector<TabInfo> tabs_;
|
||||
int current_tab_ = 0;
|
||||
|
||||
void update_tab_sensitivity();
|
||||
void refresh_visible_page();
|
||||
bool check_dirty_and_resolve();
|
||||
void switch_to_tab(int idx);
|
||||
void update_tab_buttons();
|
||||
|
||||
static void on_config_applied(void* data);
|
||||
static void on_stack_page_changed(GObject* stack, GParamSpec* pspec, gpointer data);
|
||||
static void on_project_status(const std::string& msg, void* data);
|
||||
static void on_grex_config_clicked(GtkButton* btn, gpointer data);
|
||||
};
|
||||
|
||||
@@ -31,27 +31,26 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
|
||||
gtk_paned_set_position(GTK_PANED(root_), 300);
|
||||
|
||||
// === Left panel ===
|
||||
auto* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
auto* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_size_request(left, 200, -1);
|
||||
gtk_widget_set_margin_start(left, 8);
|
||||
gtk_widget_set_margin_end(left, 4);
|
||||
gtk_widget_set_margin_top(left, 8);
|
||||
gtk_widget_set_margin_bottom(left, 8);
|
||||
|
||||
// Plan label
|
||||
plan_label_ = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(plan_label_), "<b>Plan:</b> No plan loaded");
|
||||
gtk_label_set_xalign(GTK_LABEL(plan_label_), 0.0f);
|
||||
gtk_widget_set_margin_start(plan_label_, 4);
|
||||
gtk_widget_set_margin_end(plan_label_, 4);
|
||||
gtk_widget_set_margin_top(plan_label_, 4);
|
||||
gtk_widget_add_css_class(plan_label_, "title-3");
|
||||
gtk_box_append(GTK_BOX(left), plan_label_);
|
||||
|
||||
// Plan management buttons (Open/Create shown when no plan, Close shown when plan loaded)
|
||||
auto* mgmt_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_start(mgmt_row, 4);
|
||||
gtk_widget_set_margin_end(mgmt_row, 4);
|
||||
auto* mgmt_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(mgmt_row, "linked");
|
||||
|
||||
btn_open_plan_ = gtk_button_new_with_label("Open Plan");
|
||||
btn_create_plan_ = gtk_button_new_with_label("Create Plan");
|
||||
btn_close_plan_ = gtk_button_new_with_label("Close Plan");
|
||||
btn_open_plan_ = gtk_button_new_with_label("Open");
|
||||
btn_create_plan_ = gtk_button_new_with_label("Create");
|
||||
btn_close_plan_ = gtk_button_new_with_label("Close");
|
||||
gtk_widget_set_hexpand(btn_open_plan_, TRUE);
|
||||
gtk_widget_set_hexpand(btn_create_plan_, TRUE);
|
||||
gtk_widget_set_hexpand(btn_close_plan_, TRUE);
|
||||
@@ -60,38 +59,44 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
|
||||
gtk_box_append(GTK_BOX(mgmt_row), btn_close_plan_);
|
||||
gtk_box_append(GTK_BOX(left), mgmt_row);
|
||||
|
||||
// Task controls — greyed out when no plan loaded
|
||||
task_controls_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_vexpand(task_controls_, TRUE);
|
||||
|
||||
// Task list
|
||||
auto* scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_add_css_class(scroll, "frame");
|
||||
task_listbox_ = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(task_listbox_), GTK_SELECTION_SINGLE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), task_listbox_);
|
||||
gtk_box_append(GTK_BOX(left), scroll);
|
||||
gtk_box_append(GTK_BOX(task_controls_), scroll);
|
||||
|
||||
// Buttons
|
||||
auto* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_start(btn_box, 4);
|
||||
gtk_widget_set_margin_end(btn_box, 4);
|
||||
gtk_widget_set_margin_bottom(btn_box, 4);
|
||||
// Task action buttons — grouped by function
|
||||
auto* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
|
||||
auto* btn_add = gtk_button_new_with_label("Add Task");
|
||||
auto* btn_del = gtk_button_new_with_label("Delete Task");
|
||||
auto* task_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(task_edit_group, "linked");
|
||||
auto* btn_add = gtk_button_new_with_label("Add");
|
||||
auto* btn_del = gtk_button_new_with_label("Delete");
|
||||
gtk_box_append(GTK_BOX(task_edit_group), btn_add);
|
||||
gtk_box_append(GTK_BOX(task_edit_group), btn_del);
|
||||
gtk_box_append(GTK_BOX(btn_box), task_edit_group);
|
||||
|
||||
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");
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_add);
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_del);
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_up);
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_down);
|
||||
gtk_box_append(GTK_BOX(left), btn_box);
|
||||
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);
|
||||
|
||||
// Save Plan button
|
||||
btn_save_plan_ = gtk_button_new_with_label("Save Plan");
|
||||
gtk_widget_set_margin_start(btn_save_plan_, 4);
|
||||
gtk_widget_set_margin_end(btn_save_plan_, 4);
|
||||
gtk_widget_set_margin_bottom(btn_save_plan_, 4);
|
||||
gtk_widget_set_hexpand(btn_save_plan_, TRUE);
|
||||
gtk_box_append(GTK_BOX(left), btn_save_plan_);
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_save_plan_);
|
||||
|
||||
gtk_box_append(GTK_BOX(task_controls_), btn_box);
|
||||
gtk_box_append(GTK_BOX(left), task_controls_);
|
||||
|
||||
gtk_paned_set_start_child(GTK_PANED(root_), left);
|
||||
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE);
|
||||
@@ -269,6 +274,8 @@ void PlanView::on_task_selected(GtkListBox*, GtkListBoxRow* row, gpointer data)
|
||||
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unsaved_dialog(parent);
|
||||
if (result == UnsavedResult::Cancel)
|
||||
return;
|
||||
if (result == UnsavedResult::Save)
|
||||
self->save_dirty();
|
||||
else
|
||||
@@ -374,6 +381,8 @@ void PlanView::on_close_plan(GtkButton*, gpointer data) {
|
||||
if (self->is_dirty()) {
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unsaved_dialog(parent);
|
||||
if (result == UnsavedResult::Cancel)
|
||||
return;
|
||||
if (result == UnsavedResult::Save)
|
||||
self->save_dirty();
|
||||
}
|
||||
@@ -443,6 +452,9 @@ void PlanView::update_plan_buttons() {
|
||||
gtk_widget_set_visible(btn_open_plan_, !has_plan);
|
||||
gtk_widget_set_visible(btn_create_plan_, !has_plan);
|
||||
gtk_widget_set_visible(btn_close_plan_, has_plan);
|
||||
gtk_widget_set_sensitive(task_controls_, has_plan);
|
||||
if (!has_plan)
|
||||
unit_editor_->clear();
|
||||
}
|
||||
|
||||
void PlanView::refresh() {
|
||||
|
||||
@@ -44,6 +44,7 @@ private:
|
||||
GtkWidget* btn_create_plan_;
|
||||
GtkWidget* btn_close_plan_;
|
||||
GtkWidget* btn_save_plan_;
|
||||
GtkWidget* task_controls_; // container for task list + buttons
|
||||
UnitEditor* unit_editor_;
|
||||
|
||||
int current_task_idx_ = -1;
|
||||
|
||||
@@ -17,51 +17,114 @@
|
||||
*/
|
||||
|
||||
#include "views/shells_view.h"
|
||||
#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), shells_(project.shells) {
|
||||
ShellsView::ShellsView(Project& project) : project_(project) {
|
||||
root_ = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
|
||||
gtk_paned_set_position(GTK_PANED(root_), 200);
|
||||
gtk_paned_set_position(GTK_PANED(root_), 300);
|
||||
|
||||
// Left panel: shell list
|
||||
auto* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_widget_set_size_request(left, 150, -1);
|
||||
// === Left panel: shell files ===
|
||||
auto* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_size_request(left, 200, -1);
|
||||
gtk_widget_set_margin_start(left, 8);
|
||||
gtk_widget_set_margin_end(left, 4);
|
||||
gtk_widget_set_margin_top(left, 8);
|
||||
gtk_widget_set_margin_bottom(left, 8);
|
||||
|
||||
auto* scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
listbox_ = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(listbox_), GTK_SELECTION_SINGLE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), listbox_);
|
||||
gtk_box_append(GTK_BOX(left), scroll);
|
||||
auto* file_header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(file_header), "<b>Shell Files</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(file_header), 0.0f);
|
||||
gtk_box_append(GTK_BOX(left), file_header);
|
||||
|
||||
auto* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_start(btn_box, 4);
|
||||
gtk_widget_set_margin_end(btn_box, 4);
|
||||
gtk_widget_set_margin_bottom(btn_box, 4);
|
||||
auto* btn_add = gtk_button_new_with_label("Add");
|
||||
auto* btn_del = gtk_button_new_with_label("Delete");
|
||||
auto* btn_save = gtk_button_new_with_label("Save Shells");
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_add);
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_del);
|
||||
gtk_box_append(GTK_BOX(btn_box), btn_save);
|
||||
gtk_box_append(GTK_BOX(left), btn_box);
|
||||
auto* file_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(file_scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(file_scroll, TRUE);
|
||||
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_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
|
||||
auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(file_edit_group, "linked");
|
||||
auto* btn_new_file = gtk_button_new_with_label("New");
|
||||
auto* btn_del_file = gtk_button_new_with_label("Delete");
|
||||
gtk_box_append(GTK_BOX(file_edit_group), btn_new_file);
|
||||
gtk_box_append(GTK_BOX(file_edit_group), btn_del_file);
|
||||
gtk_box_append(GTK_BOX(file_btn_box), file_edit_group);
|
||||
|
||||
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);
|
||||
|
||||
gtk_paned_set_start_child(GTK_PANED(root_), left);
|
||||
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE);
|
||||
|
||||
// Right panel: editor
|
||||
auto* right_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(right_scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
// === Right panel: shells in selected file + editor ===
|
||||
auto* right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_margin_start(right, 4);
|
||||
gtk_widget_set_margin_end(right, 8);
|
||||
gtk_widget_set_margin_top(right, 8);
|
||||
gtk_widget_set_margin_bottom(right, 8);
|
||||
|
||||
file_label_ = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(file_label_), 0.0f);
|
||||
gtk_box_append(GTK_BOX(right), file_label_);
|
||||
|
||||
auto* shell_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(shell_scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(shell_scroll, TRUE);
|
||||
gtk_widget_add_css_class(shell_scroll, "frame");
|
||||
shell_listbox_ = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(shell_listbox_), GTK_SELECTION_SINGLE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(shell_scroll), shell_listbox_);
|
||||
gtk_box_append(GTK_BOX(right), shell_scroll);
|
||||
|
||||
// Shell action buttons
|
||||
auto* shell_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
|
||||
auto* shell_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(shell_edit_group, "linked");
|
||||
auto* btn_new_shell = gtk_button_new_with_label("New");
|
||||
auto* btn_del_shell = gtk_button_new_with_label("Delete");
|
||||
gtk_box_append(GTK_BOX(shell_edit_group), btn_new_shell);
|
||||
gtk_box_append(GTK_BOX(shell_edit_group), btn_del_shell);
|
||||
gtk_box_append(GTK_BOX(shell_btn_box), shell_edit_group);
|
||||
|
||||
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");
|
||||
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);
|
||||
|
||||
// Shell properties editor
|
||||
gtk_box_append(GTK_BOX(right), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
auto* editor_header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(editor_header), "<b>Shell Properties</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(editor_header), 0.0f);
|
||||
gtk_box_append(GTK_BOX(right), editor_header);
|
||||
|
||||
editor_grid_ = gtk_grid_new();
|
||||
gtk_grid_set_row_spacing(GTK_GRID(editor_grid_), 8);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(editor_grid_), 12);
|
||||
gtk_widget_set_margin_start(editor_grid_, 16);
|
||||
gtk_widget_set_margin_end(editor_grid_, 16);
|
||||
gtk_widget_set_margin_top(editor_grid_, 16);
|
||||
gtk_widget_set_margin_bottom(editor_grid_, 16);
|
||||
|
||||
auto make_row = [&](int row, const char* label_text) -> GtkWidget* {
|
||||
auto* label = gtk_label_new(label_text);
|
||||
@@ -78,74 +141,132 @@ ShellsView::ShellsView(Project& project) : project_(project), shells_(project.sh
|
||||
entry_exec_arg_ = make_row(2, "Execution Arg");
|
||||
entry_source_cmd_ = make_row(3, "Source Cmd");
|
||||
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(right_scroll), editor_grid_);
|
||||
gtk_paned_set_end_child(GTK_PANED(root_), right_scroll);
|
||||
gtk_box_append(GTK_BOX(right), editor_grid_);
|
||||
|
||||
gtk_paned_set_end_child(GTK_PANED(root_), right);
|
||||
gtk_paned_set_shrink_end_child(GTK_PANED(root_), FALSE);
|
||||
|
||||
// Signals
|
||||
g_signal_connect(listbox_, "row-selected", G_CALLBACK(on_shell_selected), this);
|
||||
g_signal_connect(btn_add, "clicked", G_CALLBACK(on_add_shell), this);
|
||||
g_signal_connect(btn_del, "clicked", G_CALLBACK(on_delete_shell), this);
|
||||
g_signal_connect(file_listbox_, "row-selected", G_CALLBACK(on_file_selected), this);
|
||||
g_signal_connect(btn_new_file, "clicked", G_CALLBACK(on_new_file), this);
|
||||
g_signal_connect(btn_del_file, "clicked", G_CALLBACK(on_delete_file), this);
|
||||
g_signal_connect(btn_new_shell, "clicked", G_CALLBACK(on_new_shell), this);
|
||||
g_signal_connect(btn_del_shell, "clicked", G_CALLBACK(on_delete_shell), 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_save, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||
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; }
|
||||
self->load_shell(gtk_list_box_row_get_index(row));
|
||||
}), this);
|
||||
|
||||
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()) {
|
||||
self->project_.report_status("Error: no shell file selected");
|
||||
return;
|
||||
}
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
try {
|
||||
self->project_.save_shells();
|
||||
sf.save();
|
||||
self->file_dirty_ = false;
|
||||
gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
|
||||
self->project_.report_status("Saved shell file: " + sf.filepath.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
self->project_.report_status(std::string("Error: ") + e.what());
|
||||
}
|
||||
}), this);
|
||||
|
||||
// Bind entry changes to model
|
||||
auto bind = [this](GtkWidget* entry, int field) {
|
||||
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_idx_ < 0 || self->current_idx_ >= (int)self->shells_.shells.size())
|
||||
if (self->loading_ || self->current_file_idx_ < 0 || self->current_shell_idx_ < 0)
|
||||
return;
|
||||
auto& s = self->shells_.shells[self->current_idx_];
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
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));
|
||||
// determine which field by checking widget pointer
|
||||
if (GTK_WIDGET(e) == self->entry_name_) {
|
||||
s.name = text;
|
||||
// refresh list row
|
||||
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->listbox_), self->current_idx_);
|
||||
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->shell_listbox_), self->current_shell_idx_);
|
||||
if (row) {
|
||||
auto* lbl = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
|
||||
if (GTK_IS_LABEL(lbl)) gtk_label_set_text(GTK_LABEL(lbl), text.c_str());
|
||||
if (GTK_IS_LABEL(lbl)) gtk_label_set_text(GTK_LABEL(lbl), (std::string("\u25B8 ") + text).c_str());
|
||||
}
|
||||
} else if (GTK_WIDGET(e) == self->entry_path_) s.path = text;
|
||||
else if (GTK_WIDGET(e) == self->entry_exec_arg_) s.execution_arg = text;
|
||||
else if (GTK_WIDGET(e) == self->entry_source_cmd_) s.source_cmd = text;
|
||||
self->mark_file_dirty();
|
||||
}), this);
|
||||
};
|
||||
bind(entry_name_, 0);
|
||||
bind(entry_path_, 1);
|
||||
bind(entry_exec_arg_, 2);
|
||||
bind(entry_source_cmd_, 3);
|
||||
bind(entry_name_);
|
||||
bind(entry_path_);
|
||||
bind(entry_exec_arg_);
|
||||
bind(entry_source_cmd_);
|
||||
|
||||
populate_list();
|
||||
populate_file_list();
|
||||
clear_editor();
|
||||
}
|
||||
|
||||
void ShellsView::populate_list() {
|
||||
GtkWidget* child;
|
||||
while ((child = gtk_widget_get_first_child(listbox_)) != nullptr)
|
||||
gtk_list_box_remove(GTK_LIST_BOX(listbox_), child);
|
||||
void ShellsView::mark_file_dirty() {
|
||||
file_dirty_ = true;
|
||||
gtk_widget_add_css_class(btn_save_, "suggested-action");
|
||||
}
|
||||
|
||||
for (auto& s : shells_.shells) {
|
||||
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;
|
||||
|
||||
for (auto& sf : project_.shell_files) {
|
||||
auto* row = gtk_list_box_row_new();
|
||||
auto stext = std::string("\u25B8 ") + s.name;
|
||||
auto* label = gtk_label_new(stext.c_str());
|
||||
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);
|
||||
gtk_widget_set_margin_start(label, 8);
|
||||
gtk_widget_set_margin_end(label, 8);
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_widget_set_margin_bottom(label, 4);
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label);
|
||||
gtk_list_box_append(GTK_LIST_BOX(listbox_), row);
|
||||
gtk_list_box_append(GTK_LIST_BOX(file_listbox_), row);
|
||||
}
|
||||
|
||||
current_idx_ = -1;
|
||||
populate_shell_list();
|
||||
}
|
||||
|
||||
void ShellsView::populate_shell_list() {
|
||||
GtkWidget* child;
|
||||
while ((child = gtk_widget_get_first_child(shell_listbox_)) != nullptr)
|
||||
gtk_list_box_remove(GTK_LIST_BOX(shell_listbox_), child);
|
||||
|
||||
current_shell_idx_ = -1;
|
||||
clear_editor();
|
||||
|
||||
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) {
|
||||
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());
|
||||
|
||||
for (auto& s : sf.shells) {
|
||||
auto* row = gtk_list_box_row_new();
|
||||
auto text = std::string("\u25B8 ") + s.name;
|
||||
auto* label = gtk_label_new(text.c_str());
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
|
||||
gtk_widget_set_margin_start(label, 8);
|
||||
gtk_widget_set_margin_end(label, 8);
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_widget_set_margin_bottom(label, 4);
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label);
|
||||
gtk_list_box_append(GTK_LIST_BOX(shell_listbox_), row);
|
||||
}
|
||||
}
|
||||
|
||||
void ShellsView::clear_editor() {
|
||||
@@ -159,13 +280,18 @@ void ShellsView::clear_editor() {
|
||||
}
|
||||
|
||||
void ShellsView::load_shell(int idx) {
|
||||
if (idx < 0 || idx >= (int)shells_.shells.size()) {
|
||||
if (current_file_idx_ < 0 || current_file_idx_ >= (int)project_.shell_files.size()) {
|
||||
clear_editor();
|
||||
return;
|
||||
}
|
||||
auto& sf = project_.shell_files[current_file_idx_];
|
||||
if (idx < 0 || idx >= (int)sf.shells.size()) {
|
||||
clear_editor();
|
||||
return;
|
||||
}
|
||||
loading_ = true;
|
||||
current_idx_ = idx;
|
||||
auto& s = shells_.shells[idx];
|
||||
current_shell_idx_ = idx;
|
||||
auto& s = sf.shells[idx];
|
||||
gtk_widget_set_sensitive(editor_grid_, TRUE);
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry_name_), s.name.c_str());
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry_path_), s.path.c_str());
|
||||
@@ -174,37 +300,182 @@ void ShellsView::load_shell(int idx) {
|
||||
loading_ = false;
|
||||
}
|
||||
|
||||
void ShellsView::on_shell_selected(GtkListBox*, GtkListBoxRow* row, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (!row) { self->clear_editor(); return; }
|
||||
self->load_shell(gtk_list_box_row_get_index(row));
|
||||
void ShellsView::refresh() {
|
||||
project_.reload_shells();
|
||||
populate_file_list();
|
||||
}
|
||||
|
||||
void ShellsView::on_add_shell(GtkButton*, gpointer data) {
|
||||
void ShellsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (!row) {
|
||||
self->current_file_idx_ = -1;
|
||||
self->populate_shell_list();
|
||||
return;
|
||||
}
|
||||
self->current_file_idx_ = gtk_list_box_row_get_index(row);
|
||||
self->file_dirty_ = false;
|
||||
gtk_widget_remove_css_class(self->btn_save_, "suggested-action");
|
||||
self->populate_shell_list();
|
||||
}
|
||||
|
||||
void ShellsView::on_new_file(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
auto* window = gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW);
|
||||
|
||||
auto* dialog = gtk_file_dialog_new();
|
||||
gtk_file_dialog_set_title(dialog, "Create Shell File");
|
||||
gtk_file_dialog_set_accept_label(dialog, "Create");
|
||||
|
||||
auto* filter = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(filter, "Shell files (*.shells)");
|
||||
gtk_file_filter_add_pattern(filter, "*.shells");
|
||||
auto* filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
|
||||
g_list_store_append(filters, filter);
|
||||
g_object_unref(filter);
|
||||
gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters));
|
||||
g_object_unref(filters);
|
||||
|
||||
auto shells_dir = self->project_.resolved_shells_path();
|
||||
if (!shells_dir.empty()) {
|
||||
auto* initial = g_file_new_for_path(shells_dir.c_str());
|
||||
gtk_file_dialog_set_initial_folder(dialog, initial);
|
||||
g_object_unref(initial);
|
||||
}
|
||||
|
||||
gtk_file_dialog_save(dialog, GTK_WINDOW(window), nullptr, on_new_file_response, self);
|
||||
}
|
||||
|
||||
void ShellsView::on_new_file_response(GObject* source, GAsyncResult* res, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
GError* error = nullptr;
|
||||
auto* file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source), res, &error);
|
||||
if (!file) {
|
||||
if (error) g_error_free(error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto* path = g_file_get_path(file);
|
||||
g_object_unref(file);
|
||||
|
||||
std::filesystem::path fp(path);
|
||||
g_free(path);
|
||||
|
||||
if (fp.extension() != ".shells")
|
||||
fp += ".shells";
|
||||
|
||||
ShellsFile sf;
|
||||
sf.filepath = fp;
|
||||
|
||||
try {
|
||||
sf.save();
|
||||
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);
|
||||
|
||||
self->project_.report_status("Created shell file: " + fp.filename().string());
|
||||
} catch (const std::exception& e) {
|
||||
self->project_.report_status(std::string("Error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
auto name = sf.filepath.filename().string();
|
||||
|
||||
self->project_.shell_files.erase(self->project_.shell_files.begin() + self->current_file_idx_);
|
||||
self->current_file_idx_ = -1;
|
||||
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()) {
|
||||
self->project_.report_status("Error: select a shell file first");
|
||||
return;
|
||||
}
|
||||
|
||||
auto& sf = self->project_.shell_files[self->current_file_idx_];
|
||||
ShellDef s;
|
||||
s.name = "new_shell";
|
||||
s.path = "/usr/bin/new_shell";
|
||||
s.execution_arg = "-c";
|
||||
s.source_cmd = "source";
|
||||
self->shells_.shells.push_back(s);
|
||||
self->populate_list();
|
||||
sf.shells.push_back(s);
|
||||
self->populate_shell_list();
|
||||
self->mark_file_dirty();
|
||||
|
||||
int last = (int)self->shells_.shells.size() - 1;
|
||||
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->listbox_), last);
|
||||
int last = (int)sf.shells.size() - 1;
|
||||
auto* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->shell_listbox_), last);
|
||||
if (row)
|
||||
gtk_list_box_select_row(GTK_LIST_BOX(self->listbox_), row);
|
||||
gtk_list_box_select_row(GTK_LIST_BOX(self->shell_listbox_), row);
|
||||
}
|
||||
|
||||
void ShellsView::on_delete_shell(GtkButton*, gpointer data) {
|
||||
auto* self = static_cast<ShellsView*>(data);
|
||||
if (self->current_idx_ < 0) return;
|
||||
self->shells_.shells.erase(self->shells_.shells.begin() + self->current_idx_);
|
||||
self->populate_list();
|
||||
if (self->current_file_idx_ < 0 || self->current_file_idx_ >= (int)self->project_.shell_files.size())
|
||||
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_];
|
||||
if (idx < 0 || idx >= (int)sf.shells.size()) return;
|
||||
|
||||
auto name = sf.shells[idx].name;
|
||||
sf.shells.erase(sf.shells.begin() + idx);
|
||||
self->populate_shell_list();
|
||||
self->mark_file_dirty();
|
||||
self->project_.report_status("Deleted shell: " + name);
|
||||
}
|
||||
|
||||
void ShellsView::refresh() {
|
||||
populate_list();
|
||||
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;
|
||||
|
||||
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_];
|
||||
if (idx <= 0) return;
|
||||
|
||||
std::swap(sf.shells[idx], sf.shells[idx - 1]);
|
||||
self->mark_file_dirty();
|
||||
self->populate_shell_list();
|
||||
|
||||
auto* new_row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->shell_listbox_), idx - 1);
|
||||
if (new_row) gtk_list_box_select_row(GTK_LIST_BOX(self->shell_listbox_), new_row);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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_];
|
||||
if (idx < 0 || idx >= (int)sf.shells.size() - 1) return;
|
||||
|
||||
std::swap(sf.shells[idx], sf.shells[idx + 1]);
|
||||
self->mark_file_dirty();
|
||||
self->populate_shell_list();
|
||||
|
||||
auto* new_row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(self->shell_listbox_), idx + 1);
|
||||
if (new_row) gtk_list_box_select_row(GTK_LIST_BOX(self->shell_listbox_), new_row);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,26 +30,42 @@ public:
|
||||
|
||||
private:
|
||||
Project& project_;
|
||||
ShellsFile& shells_;
|
||||
GtkWidget* root_;
|
||||
GtkWidget* listbox_;
|
||||
|
||||
// Left panel: shell files
|
||||
GtkWidget* file_listbox_;
|
||||
GtkWidget* btn_save_;
|
||||
|
||||
// Right panel: shells in selected file
|
||||
GtkWidget* file_label_;
|
||||
GtkWidget* shell_listbox_;
|
||||
|
||||
// Editor
|
||||
GtkWidget* editor_grid_;
|
||||
GtkWidget* entry_name_;
|
||||
GtkWidget* entry_path_;
|
||||
GtkWidget* entry_exec_arg_;
|
||||
GtkWidget* entry_source_cmd_;
|
||||
GtkWidget* editor_grid_;
|
||||
|
||||
int current_idx_ = -1;
|
||||
int current_file_idx_ = -1;
|
||||
int current_shell_idx_ = -1;
|
||||
bool loading_ = false;
|
||||
bool file_dirty_ = false;
|
||||
|
||||
void populate_list();
|
||||
void populate_file_list();
|
||||
void populate_shell_list();
|
||||
void load_shell(int idx);
|
||||
void clear_editor();
|
||||
void mark_file_dirty();
|
||||
|
||||
static void on_shell_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);
|
||||
static void on_add_shell(GtkButton* btn, 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_response(GObject* source, GAsyncResult* res, gpointer data);
|
||||
static void on_delete_file(GtkButton* btn, gpointer data);
|
||||
static void on_new_shell(GtkButton* btn, gpointer data);
|
||||
static void on_delete_shell(GtkButton* btn, gpointer data);
|
||||
static void on_move_up(GtkButton* btn, gpointer data);
|
||||
static void on_move_down(GtkButton* btn, gpointer data);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -108,12 +108,12 @@ void UnitEditor::clear() {
|
||||
|
||||
gtk_label_set_text(GTK_LABEL(name_display_), "");
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry_comment_), "");
|
||||
gtk_widget_set_sensitive(entry_comment_, FALSE);
|
||||
gtk_widget_set_sensitive(btn_edit_unit_, FALSE);
|
||||
gtk_widget_set_sensitive(root_, FALSE);
|
||||
clear_dirty();
|
||||
}
|
||||
|
||||
void UnitEditor::load(Task* task, Unit* unit) {
|
||||
gtk_widget_set_sensitive(root_, TRUE);
|
||||
g_signal_handlers_disconnect_by_data(entry_comment_, this);
|
||||
current_task_ = task;
|
||||
current_unit_ = unit;
|
||||
@@ -142,16 +142,15 @@ void UnitEditor::load(Task* task, Unit* unit) {
|
||||
}
|
||||
|
||||
void UnitEditor::save_current() {
|
||||
if (current_unit_name_.empty()) return;
|
||||
auto* unit = project_.find_unit(current_unit_name_);
|
||||
if (!unit) return;
|
||||
current_unit_ = unit;
|
||||
if (project_.is_unit_name_taken(current_unit_->name, current_unit_)) {
|
||||
project_.report_status("Error: unit name '" + current_unit_->name + "' conflicts with another unit");
|
||||
if (!current_unit_) {
|
||||
project_.report_status("Error: no unit loaded to save");
|
||||
return;
|
||||
}
|
||||
auto* uf = project_.find_unit_file(current_unit_->name);
|
||||
if (!uf) return;
|
||||
if (!uf) {
|
||||
project_.report_status("Error: cannot find unit file for '" + current_unit_->name + "'");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
uf->save();
|
||||
clear_dirty();
|
||||
@@ -195,10 +194,19 @@ void UnitEditor::on_edit_unit(GtkButton*, gpointer data) {
|
||||
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unit_properties_dialog(parent, self->current_unit_,
|
||||
self->project_, self->grex_config_, self->project_.shells.shells);
|
||||
self->project_, self->grex_config_, self->project_.all_shells());
|
||||
|
||||
if (result == UnitDialogResult::Save)
|
||||
if (result == UnitDialogResult::Save) {
|
||||
// Sync state in case the unit name changed in the dialog
|
||||
auto new_name = self->current_unit_->name;
|
||||
self->current_unit_name_ = new_name;
|
||||
if (self->current_task_)
|
||||
self->current_task_->name = new_name;
|
||||
gtk_label_set_text(GTK_LABEL(self->name_display_), new_name.c_str());
|
||||
if (self->name_cb_)
|
||||
self->name_cb_(new_name, self->name_cb_data_);
|
||||
self->mark_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void UnitEditor::on_select_unit(GtkButton*, gpointer data) {
|
||||
|
||||
@@ -36,85 +36,92 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
|
||||
gtk_paned_set_position(GTK_PANED(root_), 300);
|
||||
|
||||
// === Left panel: unit files ===
|
||||
auto* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
auto* left = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_size_request(left, 200, -1);
|
||||
gtk_widget_set_margin_start(left, 8);
|
||||
gtk_widget_set_margin_end(left, 4);
|
||||
gtk_widget_set_margin_top(left, 8);
|
||||
gtk_widget_set_margin_bottom(left, 8);
|
||||
|
||||
auto* file_header = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(file_header), "<b>Unit Files</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(file_header), 0.0f);
|
||||
gtk_widget_set_margin_start(file_header, 4);
|
||||
gtk_widget_set_margin_end(file_header, 4);
|
||||
gtk_widget_set_margin_top(file_header, 4);
|
||||
gtk_widget_add_css_class(file_header, "title-3");
|
||||
gtk_box_append(GTK_BOX(left), file_header);
|
||||
|
||||
auto* file_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(file_scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(file_scroll, TRUE);
|
||||
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_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_start(file_btn_box, 4);
|
||||
gtk_widget_set_margin_end(file_btn_box, 4);
|
||||
gtk_widget_set_margin_bottom(file_btn_box, 4);
|
||||
auto* btn_new_file = gtk_button_new_with_label("New File");
|
||||
auto* btn_del_file = gtk_button_new_with_label("Delete File");
|
||||
gtk_box_append(GTK_BOX(file_btn_box), btn_new_file);
|
||||
gtk_box_append(GTK_BOX(file_btn_box), btn_del_file);
|
||||
// File lifecycle buttons — grouped
|
||||
auto* file_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
|
||||
auto* file_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(file_edit_group, "linked");
|
||||
auto* btn_new_file = gtk_button_new_with_label("New");
|
||||
auto* btn_del_file = gtk_button_new_with_label("Delete");
|
||||
gtk_box_append(GTK_BOX(file_edit_group), btn_new_file);
|
||||
gtk_box_append(GTK_BOX(file_edit_group), btn_del_file);
|
||||
gtk_box_append(GTK_BOX(file_btn_box), file_edit_group);
|
||||
|
||||
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);
|
||||
|
||||
gtk_paned_set_start_child(GTK_PANED(root_), left);
|
||||
gtk_paned_set_shrink_start_child(GTK_PANED(root_), FALSE);
|
||||
|
||||
// === Right panel: units in selected file ===
|
||||
auto* right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
auto* right = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_margin_start(right, 4);
|
||||
gtk_widget_set_margin_end(right, 8);
|
||||
gtk_widget_set_margin_top(right, 8);
|
||||
gtk_widget_set_margin_bottom(right, 8);
|
||||
|
||||
file_label_ = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(file_label_), "<b>No file selected</b>");
|
||||
gtk_label_set_xalign(GTK_LABEL(file_label_), 0.0f);
|
||||
gtk_widget_set_margin_start(file_label_, 4);
|
||||
gtk_widget_set_margin_end(file_label_, 4);
|
||||
gtk_widget_set_margin_top(file_label_, 4);
|
||||
gtk_widget_add_css_class(file_label_, "title-3");
|
||||
gtk_box_append(GTK_BOX(right), file_label_);
|
||||
|
||||
auto* unit_scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(unit_scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(unit_scroll, TRUE);
|
||||
gtk_widget_add_css_class(unit_scroll, "frame");
|
||||
unit_listbox_ = gtk_list_box_new();
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(unit_listbox_), GTK_SELECTION_SINGLE);
|
||||
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(unit_listbox_), FALSE);
|
||||
// No sort — units appear in file order
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(unit_scroll), unit_listbox_);
|
||||
gtk_box_append(GTK_BOX(right), unit_scroll);
|
||||
|
||||
auto* unit_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_start(unit_btn_box, 4);
|
||||
gtk_widget_set_margin_end(unit_btn_box, 4);
|
||||
gtk_widget_set_margin_bottom(unit_btn_box, 4);
|
||||
auto* btn_new_unit = gtk_button_new_with_label("New Unit");
|
||||
auto* btn_del_unit = gtk_button_new_with_label("Delete Unit");
|
||||
auto* btn_edit_unit = gtk_button_new_with_label("Edit Unit...");
|
||||
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(unit_btn_box), btn_new_unit);
|
||||
gtk_box_append(GTK_BOX(unit_btn_box), btn_del_unit);
|
||||
gtk_box_append(GTK_BOX(unit_btn_box), btn_edit_unit);
|
||||
gtk_box_append(GTK_BOX(unit_btn_box), btn_move_up);
|
||||
gtk_box_append(GTK_BOX(unit_btn_box), btn_move_down);
|
||||
gtk_box_append(GTK_BOX(right), unit_btn_box);
|
||||
// Unit action buttons — grouped by function
|
||||
auto* unit_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
|
||||
// Save button
|
||||
btn_save_ = gtk_button_new_with_label("Save Unit File");
|
||||
gtk_widget_set_margin_start(btn_save_, 4);
|
||||
gtk_widget_set_margin_end(btn_save_, 4);
|
||||
gtk_widget_set_margin_bottom(btn_save_, 4);
|
||||
gtk_widget_set_hexpand(btn_save_, TRUE);
|
||||
gtk_box_append(GTK_BOX(right), btn_save_);
|
||||
auto* unit_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
|
||||
gtk_widget_add_css_class(unit_edit_group, "linked");
|
||||
auto* btn_new_unit = gtk_button_new_with_label("New");
|
||||
auto* btn_del_unit = gtk_button_new_with_label("Delete");
|
||||
gtk_box_append(GTK_BOX(unit_edit_group), btn_new_unit);
|
||||
gtk_box_append(GTK_BOX(unit_edit_group), btn_del_unit);
|
||||
gtk_box_append(GTK_BOX(unit_btn_box), unit_edit_group);
|
||||
|
||||
auto* btn_edit_unit = gtk_button_new_with_label("Edit Unit...");
|
||||
gtk_box_append(GTK_BOX(unit_btn_box), btn_edit_unit);
|
||||
|
||||
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");
|
||||
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_paned_set_end_child(GTK_PANED(root_), right);
|
||||
gtk_paned_set_shrink_end_child(GTK_PANED(root_), FALSE);
|
||||
@@ -246,6 +253,8 @@ void UnitsView::on_file_selected(GtkListBox*, GtkListBoxRow* row, gpointer data)
|
||||
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unsaved_dialog(parent);
|
||||
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) {
|
||||
@@ -390,11 +399,14 @@ void UnitsView::on_new_unit(GtkButton*, gpointer data) {
|
||||
gtk_widget_set_halign(btn_row, GTK_ALIGN_END);
|
||||
auto* btn_cancel = gtk_button_new_with_label("Cancel");
|
||||
auto* btn_create = gtk_button_new_with_label("Create");
|
||||
gtk_widget_add_css_class(btn_create, "suggested-action");
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_cancel);
|
||||
gtk_box_append(GTK_BOX(btn_row), btn_create);
|
||||
gtk_box_append(GTK_BOX(box), btn_row);
|
||||
|
||||
gtk_window_set_child(GTK_WINDOW(win), box);
|
||||
gtk_window_set_default_widget(GTK_WINDOW(win), btn_create);
|
||||
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
|
||||
|
||||
struct NewUnitData {
|
||||
UnitsView* view;
|
||||
@@ -458,99 +470,35 @@ void UnitsView::on_delete_unit(GtkButton*, gpointer data) {
|
||||
self->project_.report_status("Deleted unit: " + name);
|
||||
}
|
||||
|
||||
void UnitsView::cancel_rename() {
|
||||
if (!rename_active_) return;
|
||||
rename_active_ = false;
|
||||
|
||||
// Restore original label
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(rename_row_), rename_old_label_);
|
||||
g_object_unref(rename_old_label_);
|
||||
rename_old_label_ = nullptr;
|
||||
rename_row_ = nullptr;
|
||||
rename_unit_idx_ = -1;
|
||||
}
|
||||
|
||||
void UnitsView::finish_rename(const std::string& new_name) {
|
||||
if (!rename_active_) return;
|
||||
rename_active_ = false;
|
||||
|
||||
auto& uf = project_.unit_files[current_file_idx_];
|
||||
auto* unit = &uf.units[rename_unit_idx_];
|
||||
|
||||
if (!new_name.empty() && new_name != unit->name) {
|
||||
if (project_.is_unit_name_taken(new_name, unit)) {
|
||||
project_.report_status("Error: unit '" + new_name + "' already exists");
|
||||
} else {
|
||||
auto old = unit->name;
|
||||
unit->name = new_name;
|
||||
file_dirty_ = true;
|
||||
gtk_widget_add_css_class(btn_save_, "suggested-action");
|
||||
project_.report_status("Renamed unit: " + old + " -> " + new_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Update label text to current name and restore it
|
||||
auto text = std::string("\u2022 ") + uf.units[rename_unit_idx_].name;
|
||||
gtk_label_set_text(GTK_LABEL(rename_old_label_), text.c_str());
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(rename_row_), rename_old_label_);
|
||||
g_object_unref(rename_old_label_);
|
||||
rename_old_label_ = nullptr;
|
||||
rename_row_ = nullptr;
|
||||
rename_unit_idx_ = -1;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Cancel any in-progress rename first
|
||||
self->cancel_rename();
|
||||
|
||||
auto& uf = self->project_.unit_files[self->current_file_idx_];
|
||||
|
||||
// Get unit name from the row's label (strip bullet prefix)
|
||||
auto* old_label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
|
||||
auto label_text = std::string(gtk_label_get_text(GTK_LABEL(old_label)));
|
||||
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
|
||||
auto label_text = std::string(gtk_label_get_text(GTK_LABEL(label)));
|
||||
if (label_text.rfind("\u2022 ", 0) == 0)
|
||||
label_text = label_text.substr(strlen("\u2022 "));
|
||||
|
||||
// Find the actual index in the units vector by name
|
||||
int idx = -1;
|
||||
for (int i = 0; i < (int)uf.units.size(); i++) {
|
||||
if (uf.units[i].name == label_text) { idx = i; break; }
|
||||
// Find unit by name
|
||||
Unit* unit = nullptr;
|
||||
for (auto& u : uf.units) {
|
||||
if (u.name == label_text) { unit = &u; break; }
|
||||
}
|
||||
if (idx < 0) return;
|
||||
if (!unit) return;
|
||||
|
||||
g_object_ref(old_label);
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unit_properties_dialog(parent, unit,
|
||||
self->project_, self->grex_config_, self->project_.all_shells());
|
||||
|
||||
self->rename_active_ = true;
|
||||
self->rename_row_ = row;
|
||||
self->rename_old_label_ = old_label;
|
||||
self->rename_unit_idx_ = idx;
|
||||
|
||||
auto* entry = gtk_entry_new();
|
||||
gtk_editable_set_text(GTK_EDITABLE(entry), uf.units[idx].name.c_str());
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), entry);
|
||||
|
||||
// Commit on Enter
|
||||
g_signal_connect(entry, "activate", G_CALLBACK(+[](GtkEntry* e, gpointer d) {
|
||||
auto* self = static_cast<UnitsView*>(d);
|
||||
self->finish_rename(gtk_editable_get_text(GTK_EDITABLE(e)));
|
||||
}), self);
|
||||
|
||||
// Cancel on focus loss — deferred to idle to avoid re-entrancy during selection
|
||||
auto* focus_controller = gtk_event_controller_focus_new();
|
||||
g_signal_connect(focus_controller, "leave", G_CALLBACK(+[](GtkEventControllerFocus*, gpointer d) {
|
||||
g_idle_add(+[](gpointer d) -> gboolean {
|
||||
auto* self = static_cast<UnitsView*>(d);
|
||||
self->cancel_rename();
|
||||
return G_SOURCE_REMOVE;
|
||||
}, d);
|
||||
}), self);
|
||||
gtk_widget_add_controller(entry, focus_controller);
|
||||
|
||||
gtk_widget_grab_focus(entry);
|
||||
if (result == UnitDialogResult::Save) {
|
||||
self->file_dirty_ = true;
|
||||
gtk_widget_add_css_class(self->btn_save_, "suggested-action");
|
||||
self->populate_unit_list();
|
||||
}
|
||||
}
|
||||
|
||||
void UnitsView::on_move_up(GtkButton*, gpointer data) {
|
||||
@@ -644,11 +592,12 @@ void UnitsView::on_edit_unit(GtkButton*, gpointer data) {
|
||||
|
||||
auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||
auto result = show_unit_properties_dialog(parent, unit,
|
||||
self->project_, self->grex_config_, self->project_.shells.shells);
|
||||
self->project_, self->grex_config_, self->project_.all_shells());
|
||||
|
||||
if (result == UnitDialogResult::Save) {
|
||||
self->file_dirty_ = true;
|
||||
gtk_widget_add_css_class(self->btn_save_, "suggested-action");
|
||||
self->populate_unit_list();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,13 +57,6 @@ private:
|
||||
static void on_edit_unit(GtkButton* btn, gpointer data);
|
||||
static void on_move_up(GtkButton* btn, gpointer data);
|
||||
static void on_move_down(GtkButton* btn, gpointer data);
|
||||
void cancel_rename();
|
||||
void finish_rename(const std::string& new_name);
|
||||
|
||||
GtkListBoxRow* rename_row_ = nullptr;
|
||||
GtkWidget* rename_old_label_ = nullptr;
|
||||
int rename_unit_idx_ = -1;
|
||||
bool rename_active_ = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user