From 219e31682200eb6f465ac0b378ae76889b1d3615 Mon Sep 17 00:00:00 2001 From: Chris Punches Date: Sat, 14 Mar 2026 18:21:33 -0400 Subject: [PATCH] Reorganize plan tab controls, add Delete Plan, context-sensitive move buttons Restructure Task Controls into UnitEditor content area with nested Underlying Unit section. Add Delete Plan button with confirmation dialog. Rename plan task buttons for clarity. Grey out Task Controls when no plan loaded. Disable Move Up/Move Down contextually based on task and unit position in both Plans and Units tabs. --- src/views/plan_view.cpp | 137 ++++++++++++++++++++++++++++++-------- src/views/plan_view.h | 5 ++ src/views/unit_editor.cpp | 50 ++++++++------ src/views/unit_editor.h | 5 ++ src/views/units_view.cpp | 34 ++++++++-- src/views/units_view.h | 3 + 6 files changed, 179 insertions(+), 55 deletions(-) diff --git a/src/views/plan_view.cpp b/src/views/plan_view.cpp index fa7e636..aed46eb 100644 --- a/src/views/plan_view.cpp +++ b/src/views/plan_view.cpp @@ -73,37 +73,25 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config) gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), task_listbox_); gtk_box_append(GTK_BOX(task_controls_), scroll); - // Task action buttons — grouped by function + // Plan Controls — plan-level actions auto* plan_ctrl_frame = gtk_frame_new("Plan Controls"); - auto* btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - gtk_widget_set_margin_start(btn_box, 8); - gtk_widget_set_margin_end(btn_box, 8); - gtk_widget_set_margin_top(btn_box, 8); - gtk_widget_set_margin_bottom(btn_box, 8); - - auto* task_edit_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - gtk_widget_add_css_class(task_edit_group, "linked"); - 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("Move Up"); - auto* btn_down = gtk_button_new_with_label("Move Down"); - gtk_box_append(GTK_BOX(move_group), btn_up); - gtk_box_append(GTK_BOX(move_group), btn_down); - gtk_box_append(GTK_BOX(btn_box), move_group); + auto* plan_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_start(plan_btn_box, 8); + gtk_widget_set_margin_end(plan_btn_box, 8); + gtk_widget_set_margin_top(plan_btn_box, 8); + gtk_widget_set_margin_bottom(plan_btn_box, 8); btn_save_plan_ = gtk_button_new_with_label("Save Plan"); - gtk_box_append(GTK_BOX(btn_box), btn_save_plan_); + gtk_box_append(GTK_BOX(plan_btn_box), btn_save_plan_); - auto* btn_refresh = gtk_button_new_with_label("Refresh"); - gtk_box_append(GTK_BOX(btn_box), btn_refresh); + auto* btn_refresh = gtk_button_new_with_label("Reload Plan"); + gtk_box_append(GTK_BOX(plan_btn_box), btn_refresh); - gtk_frame_set_child(GTK_FRAME(plan_ctrl_frame), btn_box); + auto* btn_delete_plan = gtk_button_new_with_label("Delete Plan"); + gtk_widget_add_css_class(btn_delete_plan, "destructive-action"); + gtk_box_append(GTK_BOX(plan_btn_box), btn_delete_plan); + + gtk_frame_set_child(GTK_FRAME(plan_ctrl_frame), plan_btn_box); gtk_box_append(GTK_BOX(task_controls_), plan_ctrl_frame); gtk_box_append(GTK_BOX(left), task_controls_); @@ -115,6 +103,38 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config) gtk_paned_set_end_child(GTK_PANED(root_), unit_editor_->widget()); gtk_paned_set_shrink_end_child(GTK_PANED(root_), FALSE); + // Task Controls — appended inside UnitEditor's content area + task_ctrl_frame_ = gtk_frame_new("Task Controls"); + auto* task_ctrl_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_start(task_ctrl_box, 8); + gtk_widget_set_margin_end(task_ctrl_box, 8); + gtk_widget_set_margin_top(task_ctrl_box, 8); + gtk_widget_set_margin_bottom(task_ctrl_box, 8); + + auto* task_btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + + 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 Task"); + auto* btn_del = gtk_button_new_with_label("Delete Task"); + 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(task_btn_row), task_edit_group); + + auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_add_css_class(move_group, "linked"); + btn_move_up_ = gtk_button_new_with_label("Move Up"); + btn_move_down_ = gtk_button_new_with_label("Move Down"); + gtk_box_append(GTK_BOX(move_group), btn_move_up_); + gtk_box_append(GTK_BOX(move_group), btn_move_down_); + gtk_box_append(GTK_BOX(task_btn_row), move_group); + + gtk_box_append(GTK_BOX(task_ctrl_box), task_btn_row); + gtk_box_append(GTK_BOX(task_ctrl_box), unit_editor_->unit_controls()); + + gtk_frame_set_child(GTK_FRAME(task_ctrl_frame_), task_ctrl_box); + gtk_box_append(GTK_BOX(unit_editor_->content_box()), task_ctrl_frame_); + // Name change callback to refresh the task list row label unit_editor_->set_name_changed_callback([](const std::string&, void* data) { auto* self = static_cast(data); @@ -131,8 +151,8 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config) g_signal_connect(task_listbox_, "row-selected", G_CALLBACK(on_task_selected), this); g_signal_connect(btn_add, "clicked", G_CALLBACK(on_add_task), this); g_signal_connect(btn_del, "clicked", G_CALLBACK(on_delete_task), this); - g_signal_connect(btn_up, "clicked", G_CALLBACK(on_move_up), this); - g_signal_connect(btn_down, "clicked", G_CALLBACK(on_move_down), 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_plan_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) { auto* self = static_cast(d); @@ -150,6 +170,8 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config) self->refresh(); }), this); + g_signal_connect(btn_delete_plan, "clicked", G_CALLBACK(on_delete_plan), this); + update_plan_buttons(); } @@ -183,6 +205,7 @@ void PlanView::populate_task_list() { gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label); gtk_list_box_append(GTK_LIST_BOX(task_listbox_), row); } + update_move_buttons(); } void PlanView::refresh_task_row(int idx) { @@ -216,6 +239,7 @@ void PlanView::select_task(int idx) { Unit* unit = project_.find_unit(task.name); unit_editor_->load(&task, unit); + update_move_buttons(); } // --- Open Plan --- @@ -403,6 +427,50 @@ void PlanView::on_close_plan(GtkButton*, gpointer data) { self->close_plan_impl(); } +void PlanView::on_delete_plan(GtkButton*, gpointer data) { + auto* self = static_cast(data); + auto* plan = self->current_plan(); + if (!plan) { + self->project_.report_status("Error: no plan loaded"); + return; + } + + auto filepath = plan->filepath; + auto name = filepath.filename().string(); + + // Confirm deletion + auto* parent = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW)); + auto* dialog = gtk_alert_dialog_new("Delete plan file '%s' from disk?", name.c_str()); + gtk_alert_dialog_set_detail(dialog, "This action cannot be undone."); + gtk_alert_dialog_set_buttons(dialog, (const char*[]){"Cancel", "Delete", nullptr}); + gtk_alert_dialog_set_cancel_button(dialog, 0); + gtk_alert_dialog_set_default_button(dialog, 0); + + struct DeleteCtx { PlanView* view; std::filesystem::path path; std::string name; }; + auto* ctx = new DeleteCtx{self, filepath, name}; + + gtk_alert_dialog_choose(dialog, parent, nullptr, + +[](GObject* source, GAsyncResult* res, gpointer d) { + auto* ctx = static_cast(d); + GError* error = nullptr; + int choice = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(source), res, &error); + if (error) { g_error_free(error); delete ctx; return; } + if (choice != 1) { delete ctx; return; } // not "Delete" + + // Close the plan first + ctx->view->close_plan_impl(); + + // Delete from disk + std::error_code ec; + if (std::filesystem::remove(ctx->path, ec)) + ctx->view->project_.report_status("Deleted plan file: " + ctx->name); + else + ctx->view->project_.report_status("Error: could not delete " + ctx->name + + (ec ? ": " + ec.message() : "")); + delete ctx; + }, ctx); +} + void PlanView::on_create_plan(GtkButton*, gpointer data) { auto* self = static_cast(data); auto* window = gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW); @@ -467,10 +535,23 @@ void PlanView::update_plan_buttons() { 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); + gtk_widget_set_sensitive(task_ctrl_frame_, has_plan); if (!has_plan) unit_editor_->clear(); } +void PlanView::update_move_buttons() { + auto* plan = current_plan(); + if (!plan || current_task_idx_ < 0) { + gtk_widget_set_sensitive(btn_move_up_, FALSE); + gtk_widget_set_sensitive(btn_move_down_, FALSE); + return; + } + int count = (int)plan->tasks.size(); + gtk_widget_set_sensitive(btn_move_up_, current_task_idx_ > 0); + gtk_widget_set_sensitive(btn_move_down_, current_task_idx_ < count - 1); +} + void PlanView::refresh() { // reload units if paths now resolve project_.load_all_units(); diff --git a/src/views/plan_view.h b/src/views/plan_view.h index 3f21420..30d469a 100644 --- a/src/views/plan_view.h +++ b/src/views/plan_view.h @@ -45,6 +45,9 @@ private: GtkWidget* btn_close_plan_; GtkWidget* btn_save_plan_; GtkWidget* task_controls_; // container for task list + buttons + GtkWidget* task_ctrl_frame_; // Task Controls frame in right panel + GtkWidget* btn_move_up_; + GtkWidget* btn_move_down_; UnitEditor* unit_editor_; int current_task_idx_ = -1; @@ -64,11 +67,13 @@ private: static void on_move_up(GtkButton* btn, gpointer data); static void on_move_down(GtkButton* btn, gpointer data); static void on_close_plan(GtkButton* btn, gpointer data); + static void on_delete_plan(GtkButton* btn, gpointer data); static void on_create_plan(GtkButton* btn, gpointer data); static void on_create_plan_response(GObject* source, GAsyncResult* res, gpointer data); void refresh_task_row(int idx); void update_plan_buttons(); + void update_move_buttons(); void close_plan_impl(); }; diff --git a/src/views/unit_editor.cpp b/src/views/unit_editor.cpp index 216b970..2f3f2d2 100644 --- a/src/views/unit_editor.cpp +++ b/src/views/unit_editor.cpp @@ -27,17 +27,20 @@ UnitEditor::UnitEditor(Project& project, GrexConfig& grex_config) root_ = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(root_), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); - auto* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); - 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); + content_box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_set_margin_start(content_box_, 16); + gtk_widget_set_margin_end(content_box_, 16); + gtk_widget_set_margin_top(content_box_, 16); + gtk_widget_set_margin_bottom(content_box_, 16); + auto* box = content_box_; + + // Task properties container — disabled when no task selected + task_section_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); - // Task section header auto* task_label = gtk_label_new(nullptr); gtk_label_set_markup(GTK_LABEL(task_label), "Task Properties"); gtk_label_set_xalign(GTK_LABEL(task_label), 0.0f); - gtk_box_append(GTK_BOX(box), task_label); + gtk_box_append(GTK_BOX(task_section_), task_label); auto* task_grid = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(task_grid), 6); @@ -60,30 +63,33 @@ UnitEditor::UnitEditor(Project& project, GrexConfig& grex_config) gtk_widget_set_hexpand(entry_comment_, TRUE); gtk_grid_attach(GTK_GRID(task_grid), entry_comment_, 1, 1, 1, 1); - // Change/Select Unit button — aligned with the value column + gtk_box_append(GTK_BOX(task_section_), task_grid); + gtk_box_append(GTK_BOX(box), task_section_); + + // Underlying Unit controls — exposed for external placement + unit_controls_ = gtk_frame_new("Underlying Unit"); + auto* unit_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_start(unit_btn_box, 8); + gtk_widget_set_margin_end(unit_btn_box, 8); + gtk_widget_set_margin_top(unit_btn_box, 8); + gtk_widget_set_margin_bottom(unit_btn_box, 8); + btn_select_unit_ = gtk_button_new_with_label("Change/Select Unit..."); - gtk_widget_set_halign(btn_select_unit_, GTK_ALIGN_START); g_signal_connect(btn_select_unit_, "clicked", G_CALLBACK(on_select_unit), this); - gtk_grid_attach(GTK_GRID(task_grid), btn_select_unit_, 1, 2, 1, 1); + gtk_box_append(GTK_BOX(unit_btn_box), btn_select_unit_); - gtk_box_append(GTK_BOX(box), task_grid); - - // Buttons below task properties - gtk_box_append(GTK_BOX(box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)); - - auto* btn_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); btn_edit_unit_ = gtk_button_new_with_label("Edit Unit..."); g_signal_connect(btn_edit_unit_, "clicked", G_CALLBACK(on_edit_unit), this); - gtk_box_append(GTK_BOX(btn_row), btn_edit_unit_); + gtk_box_append(GTK_BOX(unit_btn_box), btn_edit_unit_); btn_save_unit_ = gtk_button_new_with_label("Save Unit"); g_signal_connect(btn_save_unit_, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) { auto* self = static_cast(d); self->save_current(); }), this); - gtk_box_append(GTK_BOX(btn_row), btn_save_unit_); + gtk_box_append(GTK_BOX(unit_btn_box), btn_save_unit_); - gtk_box_append(GTK_BOX(box), btn_row); + gtk_frame_set_child(GTK_FRAME(unit_controls_), unit_btn_box); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(root_), box); @@ -108,12 +114,14 @@ void UnitEditor::clear() { gtk_label_set_text(GTK_LABEL(name_display_), ""); gtk_editable_set_text(GTK_EDITABLE(entry_comment_), ""); - gtk_widget_set_sensitive(root_, FALSE); + gtk_widget_set_sensitive(task_section_, FALSE); + gtk_widget_set_sensitive(unit_controls_, FALSE); clear_dirty(); } void UnitEditor::load(Task* task, Unit* unit) { - gtk_widget_set_sensitive(root_, TRUE); + gtk_widget_set_sensitive(task_section_, TRUE); + gtk_widget_set_sensitive(unit_controls_, TRUE); g_signal_handlers_disconnect_by_data(entry_comment_, this); current_task_ = task; current_unit_ = unit; diff --git a/src/views/unit_editor.h b/src/views/unit_editor.h index 0db4c87..08b04ea 100644 --- a/src/views/unit_editor.h +++ b/src/views/unit_editor.h @@ -29,6 +29,8 @@ public: UnitEditor(Project& project, GrexConfig& grex_config); ~UnitEditor() = default; GtkWidget* widget() { return root_; } + GtkWidget* content_box() { return content_box_; } + GtkWidget* unit_controls() { return unit_controls_; } void load(Task* task, Unit* unit); void clear(); @@ -49,6 +51,9 @@ private: Unit* current_unit_ = nullptr; std::string current_unit_name_; GtkWidget* root_; + GtkWidget* content_box_; + GtkWidget* unit_controls_; + GtkWidget* task_section_; // task fields GtkWidget* name_display_; diff --git a/src/views/units_view.cpp b/src/views/units_view.cpp index 0743270..f1cf828 100644 --- a/src/views/units_view.cpp +++ b/src/views/units_view.cpp @@ -123,10 +123,10 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config) auto* move_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_add_css_class(move_group, "linked"); - auto* btn_move_up = gtk_button_new_with_label("Move Up"); - auto* btn_move_down = gtk_button_new_with_label("Move Down"); - gtk_box_append(GTK_BOX(move_group), btn_move_up); - gtk_box_append(GTK_BOX(move_group), btn_move_down); + btn_move_up_ = gtk_button_new_with_label("Move Up"); + btn_move_down_ = gtk_button_new_with_label("Move Down"); + gtk_box_append(GTK_BOX(move_group), btn_move_up_); + gtk_box_append(GTK_BOX(move_group), btn_move_down_); gtk_box_append(GTK_BOX(unit_btn_box), move_group); gtk_frame_set_child(GTK_FRAME(unit_ctrl_frame), unit_btn_box); @@ -142,9 +142,12 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config) g_signal_connect(btn_new_unit, "clicked", G_CALLBACK(on_new_unit), this); g_signal_connect(btn_del_unit, "clicked", G_CALLBACK(on_delete_unit), this); g_signal_connect(btn_edit_unit, "clicked", G_CALLBACK(on_edit_unit), 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_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(unit_listbox_, "row-activated", G_CALLBACK(on_unit_activated), this); + g_signal_connect(unit_listbox_, "row-selected", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow*, gpointer d) { + static_cast(d)->update_move_buttons(); + }), this); g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) { auto* self = static_cast(d); @@ -238,6 +241,25 @@ void UnitsView::populate_unit_list() { gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label); gtk_list_box_append(GTK_LIST_BOX(unit_listbox_), row); } + update_move_buttons(); +} + +void UnitsView::update_move_buttons() { + if (!selected_file_) { + gtk_widget_set_sensitive(btn_move_up_, FALSE); + gtk_widget_set_sensitive(btn_move_down_, FALSE); + return; + } + auto* row = gtk_list_box_get_selected_row(GTK_LIST_BOX(unit_listbox_)); + if (!row) { + gtk_widget_set_sensitive(btn_move_up_, FALSE); + gtk_widget_set_sensitive(btn_move_down_, FALSE); + return; + } + int idx = gtk_list_box_row_get_index(row); + int count = (int)selected_file_->units.size(); + gtk_widget_set_sensitive(btn_move_up_, idx > 0); + gtk_widget_set_sensitive(btn_move_down_, idx < count - 1); } void UnitsView::refresh() { diff --git a/src/views/units_view.h b/src/views/units_view.h index 11ed1d8..aad5ec9 100644 --- a/src/views/units_view.h +++ b/src/views/units_view.h @@ -39,6 +39,8 @@ private: GtkWidget* unit_listbox_; GtkWidget* file_label_; GtkWidget* btn_save_; + GtkWidget* btn_move_up_; + GtkWidget* btn_move_down_; UnitFile* selected_file_ = nullptr; bool file_dirty_ = false; @@ -46,6 +48,7 @@ private: void populate_file_list(); void populate_unit_list(); + void update_move_buttons(); GtkListBoxRow* find_file_row(UnitFile* uf); static void on_file_selected(GtkListBox* box, GtkListBoxRow* row, gpointer data);