diff --git a/src/models/project.cpp b/src/models/project.cpp index 7c46e0a..d298e3d 100644 --- a/src/models/project.cpp +++ b/src/models/project.cpp @@ -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 Project::all_shells() const { + std::vector 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"); } diff --git a/src/models/project.h b/src/models/project.h index 24a5731..b99e249 100644 --- a/src/models/project.h +++ b/src/models/project.h @@ -37,7 +37,7 @@ public: std::vector plans; std::vector unit_files; - ShellsFile shells; + std::vector 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 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(); diff --git a/src/util/unit_properties_dialog.cpp b/src/util/unit_properties_dialog.cpp index f5f410b..0d48b1b 100644 --- a/src/util/unit_properties_dialog.cpp +++ b/src/util/unit_properties_dialog.cpp @@ -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(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& shells, const std::string& name) { @@ -110,6 +114,7 @@ static int shell_index(const std::vector& 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("") + unit->name + ""; - 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(" ") + title + " ").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(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)); diff --git a/src/util/unsaved_dialog.cpp b/src/util/unsaved_dialog.cpp index 0ce771d..072593d 100644 --- a/src/util/unsaved_dialog.cpp +++ b/src/util/unsaved_dialog.cpp @@ -17,6 +17,7 @@ */ #include "util/unsaved_dialog.h" +#include namespace grex { @@ -26,46 +27,58 @@ struct DialogState { GtkWidget* window; }; -static void on_revert_clicked(GtkButton*, gpointer d) { - auto* state = static_cast(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(d); + gtk_window_present(GTK_WINDOW(dialog_win)); } -static void on_save_clicked(GtkButton*, gpointer d) { - auto* state = static_cast(d); - state->result = UnsavedResult::Save; - gtk_window_close(GTK_WINDOW(state->window)); -} - -static void on_dialog_closed(GtkWidget*, gpointer d) { - auto* state = static_cast(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), "Unsaved Changes"); + 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(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(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; } diff --git a/src/util/unsaved_dialog.h b/src/util/unsaved_dialog.h index e8682e8..89fc3ba 100644 --- a/src/util/unsaved_dialog.h +++ b/src/util/unsaved_dialog.h @@ -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); diff --git a/src/views/config_view.cpp b/src/views/config_view.cpp index dcc2d12..285c65d 100644 --- a/src/views/config_view.cpp +++ b/src/views/config_view.cpp @@ -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), "Configuration"); 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), "Resolved Paths"); 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), "Variables"); 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(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(); } diff --git a/src/views/config_view.h b/src/views/config_view.h index 34a4cc3..823be05 100644 --- a/src/views/config_view.h +++ b/src/views/config_view.h @@ -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(); diff --git a/src/views/main_window.cpp b/src/views/main_window.cpp index 5405714..f003c1d 100644 --- a/src/views/main_window.cpp +++ b/src/views/main_window.cpp @@ -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(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(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(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), "GREX Preferences"); + 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}; diff --git a/src/views/main_window.h b/src/views/main_window.h index e54e4ea..57619b9 100644 --- a/src/views/main_window.h +++ b/src/views/main_window.h @@ -19,6 +19,7 @@ #pragma once #include #include +#include #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 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); }; diff --git a/src/views/plan_view.cpp b/src/views/plan_view.cpp index 1db90a6..728af8a 100644 --- a/src/views/plan_view.cpp +++ b/src/views/plan_view.cpp @@ -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_), "Plan: 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() { diff --git a/src/views/plan_view.h b/src/views/plan_view.h index 679f157..3f21420 100644 --- a/src/views/plan_view.h +++ b/src/views/plan_view.h @@ -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; diff --git a/src/views/shells_view.cpp b/src/views/shells_view.cpp index f2fcac6..2116ad0 100644 --- a/src/views/shells_view.cpp +++ b/src/views/shells_view.cpp @@ -17,51 +17,114 @@ */ #include "views/shells_view.h" +#include + +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), "Shell Files"); + 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_), "No file selected"); + 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), "Shell Properties"); + 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(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(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(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_), "No file selected"); + return; + } + + auto& sf = project_.shell_files[current_file_idx_]; + gtk_label_set_markup(GTK_LABEL(file_label_), + (std::string("") + sf.filepath.filename().string() + "").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(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(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(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(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(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(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(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(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(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); } } diff --git a/src/views/shells_view.h b/src/views/shells_view.h index 3f4e545..921848c 100644 --- a/src/views/shells_view.h +++ b/src/views/shells_view.h @@ -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); }; } diff --git a/src/views/unit_editor.cpp b/src/views/unit_editor.cpp index 031e188..216b970 100644 --- a/src/views/unit_editor.cpp +++ b/src/views/unit_editor.cpp @@ -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) { diff --git a/src/views/units_view.cpp b/src/views/units_view.cpp index 82157cf..cbce62e 100644 --- a/src/views/units_view.cpp +++ b/src/views/units_view.cpp @@ -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), "Unit Files"); 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_), "No file selected"); 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(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(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(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(); } } diff --git a/src/views/units_view.h b/src/views/units_view.h index bbb3079..2b5f441 100644 --- a/src/views/units_view.h +++ b/src/views/units_view.h @@ -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; }; }