Add unit validation with red highlighting and status bar feedback

Units that cannot be executed are shown in red on both the Units and
Plans tabs.  Validation checks target existence/executability, shell
definition lookup, working directory existence, rectifier
existence/executability, and environment file existence.

The unit properties dialog applies live validation with GTK error
styling on each field and reports issues to the status bar as the
user edits.  Selecting a unit or task re-runs validation so the
status bar always reflects the selected item.

Also separates plan dirty state from unit editor dirty state so
editing unit properties no longer marks the plan as unsaved.
This commit is contained in:
Chris Punches
2026-03-16 03:21:25 -04:00
parent 219e316822
commit e852b7e182
10 changed files with 274 additions and 53 deletions

View File

@@ -18,6 +18,7 @@
#include "models/project.h" #include "models/project.h"
#include <set> #include <set>
#include <unistd.h>
namespace grex { namespace grex {
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -50,6 +51,73 @@ fs::path Project::resolved_shells_path() const {
return root / sp; return root / sp;
} }
bool Project::check_unit_valid(const Unit& u) {
namespace fs = std::filesystem;
if (u.target.empty()) {
report_status("Unit '" + u.name + "': target is not defined");
return false;
}
if (!fs::exists(u.target)) {
report_status("Unit '" + u.name + "': target does not exist: " + u.target);
return false;
}
if (access(u.target.c_str(), X_OK) != 0) {
report_status("Unit '" + u.name + "': target is not executable: " + u.target);
return false;
}
if (u.is_shell_command) {
bool found = false;
for (auto& s : all_shells()) {
if (s.name == u.shell_definition) { found = true; break; }
}
if (!found) {
report_status("Unit '" + u.name + "': shell definition not found: " + u.shell_definition);
return false;
}
}
if (u.set_working_directory) {
if (u.working_directory.empty()) {
report_status("Unit '" + u.name + "': working directory is not defined");
return false;
}
if (!fs::is_directory(u.working_directory)) {
report_status("Unit '" + u.name + "': working directory does not exist: " + u.working_directory);
return false;
}
}
if (u.rectify) {
if (u.rectifier.empty()) {
report_status("Unit '" + u.name + "': rectifier is not defined");
return false;
}
if (!fs::exists(u.rectifier)) {
report_status("Unit '" + u.name + "': rectifier does not exist: " + u.rectifier);
return false;
}
if (access(u.rectifier.c_str(), X_OK) != 0) {
report_status("Unit '" + u.name + "': rectifier is not executable: " + u.rectifier);
return false;
}
}
if (u.supply_environment) {
if (u.environment.empty()) {
report_status("Unit '" + u.name + "': environment file is not defined");
return false;
}
if (!fs::exists(u.environment)) {
report_status("Unit '" + u.name + "': environment file does not exist: " + u.environment);
return false;
}
}
return true;
}
Unit* Project::find_unit(const std::string& unit_name) { Unit* Project::find_unit(const std::string& unit_name) {
for (auto& uf : unit_files) { for (auto& uf : unit_files) {
auto* u = uf.find_unit(unit_name); auto* u = uf.find_unit(unit_name);

View File

@@ -50,6 +50,7 @@ public:
std::filesystem::path resolved_units_dir() const; std::filesystem::path resolved_units_dir() const;
std::filesystem::path resolved_shells_path() const; std::filesystem::path resolved_shells_path() const;
bool check_unit_valid(const Unit& u);
Unit* find_unit(const std::string& unit_name); Unit* find_unit(const std::string& unit_name);
UnitFile* find_unit_file(const std::string& unit_name); UnitFile* find_unit_file(const std::string& unit_name);
bool is_unit_name_taken(const std::string& name, const Unit* exclude = nullptr) const; bool is_unit_name_taken(const std::string& name, const Unit* exclude = nullptr) const;

View File

@@ -19,6 +19,7 @@
#include "util/unit_properties_dialog.h" #include "util/unit_properties_dialog.h"
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <unistd.h>
namespace grex { namespace grex {
@@ -93,14 +94,17 @@ struct DialogState {
}; };
static void update_sensitivity(DialogState* s); static void update_sensitivity(DialogState* s);
static void validate_fields(DialogState* s);
static gboolean dlg_switch_state_set_cb(GtkSwitch* sw, gboolean new_state, gpointer data) { static gboolean dlg_switch_state_set_cb(GtkSwitch* sw, gboolean new_state, gpointer data) {
auto* b = static_cast<DlgSwitchBinding*>(data); auto* b = static_cast<DlgSwitchBinding*>(data);
gtk_switch_set_state(sw, new_state); gtk_switch_set_state(sw, new_state);
if (b->state) { if (b->state) {
*b->target = new_state; *b->target = new_state;
if (!b->state->loading) if (!b->state->loading) {
update_sensitivity(b->state); update_sensitivity(b->state);
validate_fields(b->state);
}
} }
return TRUE; return TRUE;
} }
@@ -361,6 +365,102 @@ static void update_sensitivity(DialogState* s) {
show(active && s->working_copy.supply_environment, {s->label_environment, s->box_environment}); show(active && s->working_copy.supply_environment, {s->label_environment, s->box_environment});
} }
static void validate_fields(DialogState* s) {
if (s->loading) return;
auto& u = s->working_copy;
namespace fs = std::filesystem;
auto set_valid = [](GtkWidget* w, bool valid) {
if (valid)
gtk_widget_remove_css_class(w, "error");
else
gtk_widget_add_css_class(w, "error");
};
std::string error_msg;
// Target: must be defined, exist, and be executable
{
bool valid = true;
if (u.target.empty()) {
valid = false;
if (error_msg.empty()) error_msg = "Target is not defined";
} else if (!fs::exists(u.target)) {
valid = false;
if (error_msg.empty()) error_msg = "Target does not exist: " + u.target;
} else if (access(u.target.c_str(), X_OK) != 0) {
valid = false;
if (error_msg.empty()) error_msg = "Target is not executable: " + u.target;
}
set_valid(s->entry_target, valid);
}
// Shell definition: if shell command, must reference a known shell
if (u.is_shell_command) {
bool valid = false;
for (auto& sh : *s->shells) {
if (sh.name == u.shell_definition) { valid = true; break; }
}
set_valid(s->dropdown_shell_def, valid);
if (!valid && error_msg.empty())
error_msg = "Shell definition not found: " + u.shell_definition;
} else {
set_valid(s->dropdown_shell_def, true);
}
// Working directory: must exist as a directory if enabled
if (u.set_working_directory) {
bool valid = true;
if (u.working_directory.empty()) {
valid = false;
if (error_msg.empty()) error_msg = "Working directory is not defined";
} else if (!fs::is_directory(u.working_directory)) {
valid = false;
if (error_msg.empty()) error_msg = "Working directory does not exist: " + u.working_directory;
}
set_valid(s->entry_workdir, valid);
} else {
set_valid(s->entry_workdir, true);
}
// Rectifier: must exist and be executable if rectification enabled
if (u.rectify) {
bool valid = true;
if (u.rectifier.empty()) {
valid = false;
if (error_msg.empty()) error_msg = "Rectifier is not defined";
} else if (!fs::exists(u.rectifier)) {
valid = false;
if (error_msg.empty()) error_msg = "Rectifier does not exist: " + u.rectifier;
} else if (access(u.rectifier.c_str(), X_OK) != 0) {
valid = false;
if (error_msg.empty()) error_msg = "Rectifier is not executable: " + u.rectifier;
}
set_valid(s->entry_rectifier, valid);
} else {
set_valid(s->entry_rectifier, true);
}
// Environment file: must exist if supply_environment active
if (u.supply_environment) {
bool valid = true;
if (u.environment.empty()) {
valid = false;
if (error_msg.empty()) error_msg = "Environment file is not defined";
} else if (!fs::exists(u.environment)) {
valid = false;
if (error_msg.empty()) error_msg = "Environment file does not exist: " + u.environment;
}
set_valid(s->entry_environment, valid);
} else {
set_valid(s->entry_environment, true);
}
if (!error_msg.empty())
s->project->report_status(error_msg);
}
static void populate_and_connect(DialogState* s) { static void populate_and_connect(DialogState* s) {
auto& u = s->working_copy; auto& u = s->working_copy;
@@ -390,6 +490,7 @@ static void populate_and_connect(DialogState* s) {
gtk_editable_set_text(GTK_EDITABLE(s->entry_environment), u.environment.c_str()); gtk_editable_set_text(GTK_EDITABLE(s->entry_environment), u.environment.c_str());
update_sensitivity(s); update_sensitivity(s);
s->loading = false; s->loading = false;
validate_fields(s);
// Entry bindings // Entry bindings
auto bind_entry = [s](GtkWidget* entry, std::string* target) { auto bind_entry = [s](GtkWidget* entry, std::string* target) {
@@ -398,6 +499,7 @@ static void populate_and_connect(DialogState* s) {
g_signal_connect(entry, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) { g_signal_connect(entry, "changed", G_CALLBACK(+[](GtkEditable* e, gpointer d) {
auto* eb = static_cast<DlgEntryBinding*>(d); auto* eb = static_cast<DlgEntryBinding*>(d);
*eb->target = gtk_editable_get_text(e); *eb->target = gtk_editable_get_text(e);
validate_fields(eb->state);
}), eb); }), eb);
}; };
bind_entry(s->entry_name, &u.name); bind_entry(s->entry_name, &u.name);
@@ -430,6 +532,7 @@ static void populate_and_connect(DialogState* s) {
auto idx = gtk_drop_down_get_selected(GTK_DROP_DOWN(obj)); auto idx = gtk_drop_down_get_selected(GTK_DROP_DOWN(obj));
if (idx < s->shells->size()) if (idx < s->shells->size())
s->working_copy.shell_definition = (*s->shells)[idx].name; s->working_copy.shell_definition = (*s->shells)[idx].name;
validate_fields(s);
}), s); }), s);
} }

View File

@@ -225,16 +225,7 @@ void MainWindow::refresh_visible_page() {
// Refresh the newly visible page // Refresh the newly visible page
if (visible == plan_view_->widget()) { if (visible == plan_view_->widget()) {
project_.load_all_units(); plan_view_->reload_plan_from_disk();
if (!project_.plans.empty()) {
auto& plan = project_.plans[0];
try {
plan = Plan::load(plan.filepath);
} catch (const std::exception& e) {
project_.report_status(std::string("Error reloading plan: ") + e.what());
}
}
plan_view_->refresh();
} else if (visible == units_view_->widget()) { } else if (visible == units_view_->widget()) {
units_view_->refresh(); units_view_->refresh();
} else if (visible == shells_view_->widget()) { } else if (visible == shells_view_->widget()) {

View File

@@ -135,11 +135,13 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
gtk_frame_set_child(GTK_FRAME(task_ctrl_frame_), task_ctrl_box); gtk_frame_set_child(GTK_FRAME(task_ctrl_frame_), task_ctrl_box);
gtk_box_append(GTK_BOX(unit_editor_->content_box()), task_ctrl_frame_); gtk_box_append(GTK_BOX(unit_editor_->content_box()), task_ctrl_frame_);
// Name change callback to refresh the task list row label // Callback fired after any unit edit in the dialog
unit_editor_->set_name_changed_callback([](const std::string&, void* data) { unit_editor_->set_unit_edited_callback([](const std::string&, bool name_changed, void* data) {
auto* self = static_cast<PlanView*>(data); auto* self = static_cast<PlanView*>(data);
if (name_changed) {
self->plan_dirty_ = true; self->plan_dirty_ = true;
gtk_widget_add_css_class(self->btn_save_plan_, "suggested-action"); gtk_widget_add_css_class(self->btn_save_plan_, "suggested-action");
}
if (self->current_task_idx_ >= 0) if (self->current_task_idx_ >= 0)
self->refresh_task_row(self->current_task_idx_); self->refresh_task_row(self->current_task_idx_);
}, this); }, this);
@@ -167,7 +169,7 @@ PlanView::PlanView(Project& project, GrexConfig& grex_config)
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) { g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
auto* self = static_cast<PlanView*>(d); auto* self = static_cast<PlanView*>(d);
self->refresh(); self->reload_plan_from_disk();
}), this); }), this);
g_signal_connect(btn_delete_plan, "clicked", G_CALLBACK(on_delete_plan), this); g_signal_connect(btn_delete_plan, "clicked", G_CALLBACK(on_delete_plan), this);
@@ -195,8 +197,17 @@ void PlanView::populate_task_list() {
for (auto& task : plan->tasks) { for (auto& task : plan->tasks) {
auto* row = gtk_list_box_row_new(); auto* row = gtk_list_box_row_new();
auto text = std::string("\u25B6 ") + task.name; auto* label = gtk_label_new(nullptr);
auto* label = gtk_label_new(text.c_str()); auto* escaped = g_markup_escape_text(task.name.c_str(), -1);
Unit* unit = project_.find_unit(task.name);
bool valid = unit && project_.check_unit_valid(*unit);
std::string markup;
if (valid)
markup = std::string("\u25B6 ") + escaped;
else
markup = std::string("<span foreground=\"red\">\u25B6 ") + escaped + "</span>";
g_free(escaped);
gtk_label_set_markup(GTK_LABEL(label), markup.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 0.0f); gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
gtk_widget_set_margin_start(label, 8); gtk_widget_set_margin_start(label, 8);
gtk_widget_set_margin_end(label, 8); gtk_widget_set_margin_end(label, 8);
@@ -217,8 +228,17 @@ void PlanView::refresh_task_row(int idx) {
auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row)); auto* label = gtk_list_box_row_get_child(GTK_LIST_BOX_ROW(row));
if (GTK_IS_LABEL(label)) { if (GTK_IS_LABEL(label)) {
auto text = std::string("\u25B6 ") + plan->tasks[idx].name; auto& task = plan->tasks[idx];
gtk_label_set_text(GTK_LABEL(label), text.c_str()); auto* escaped = g_markup_escape_text(task.name.c_str(), -1);
Unit* unit = project_.find_unit(task.name);
bool valid = unit && project_.check_unit_valid(*unit);
std::string markup;
if (valid)
markup = std::string("\u25B6 ") + escaped;
else
markup = std::string("<span foreground=\"red\">\u25B6 ") + escaped + "</span>";
g_free(escaped);
gtk_label_set_markup(GTK_LABEL(label), markup.c_str());
} }
} }
@@ -237,6 +257,10 @@ void PlanView::select_task(int idx) {
project_.load_all_units(); project_.load_all_units();
Unit* unit = project_.find_unit(task.name); Unit* unit = project_.find_unit(task.name);
if (unit)
project_.check_unit_valid(*unit);
else
project_.report_status("Unit not found: " + task.name);
unit_editor_->load(&task, unit); unit_editor_->load(&task, unit);
update_move_buttons(); update_move_buttons();
@@ -552,23 +576,27 @@ void PlanView::update_move_buttons() {
gtk_widget_set_sensitive(btn_move_down_, current_task_idx_ < count - 1); gtk_widget_set_sensitive(btn_move_down_, current_task_idx_ < count - 1);
} }
void PlanView::refresh() { void PlanView::reload_plan_from_disk() {
// reload units if paths now resolve
project_.load_all_units();
// reload the plan from disk if one is loaded
auto* plan = current_plan(); auto* plan = current_plan();
if (plan) { if (!plan) return;
try { try {
*plan = Plan::load(plan->filepath); *plan = Plan::load(plan->filepath);
project_.report_status("Reloaded plan: " + plan->filepath.filename().string()); project_.report_status("Reloaded plan: " + plan->filepath.filename().string());
} catch (const std::exception& e) { } catch (const std::exception& e) {
project_.report_status("Error reloading plan: " + std::string(e.what())); project_.report_status("Error reloading plan: " + std::string(e.what()));
} }
refresh();
}
void PlanView::refresh() {
// reload units if paths now resolve
project_.load_all_units();
auto* plan = current_plan();
if (plan)
gtk_label_set_markup(GTK_LABEL(plan_label_), (std::string("<b>Plan:</b> ") + plan->filepath.filename().string()).c_str()); gtk_label_set_markup(GTK_LABEL(plan_label_), (std::string("<b>Plan:</b> ") + plan->filepath.filename().string()).c_str());
} else { else
gtk_label_set_markup(GTK_LABEL(plan_label_), "<b>Plan:</b> No plan loaded"); gtk_label_set_markup(GTK_LABEL(plan_label_), "<b>Plan:</b> No plan loaded");
}
populate_task_list(); populate_task_list();
update_plan_buttons(); update_plan_buttons();
@@ -577,7 +605,7 @@ void PlanView::refresh() {
} }
bool PlanView::is_dirty() const { bool PlanView::is_dirty() const {
return plan_dirty_ || unit_editor_->is_dirty(); return plan_dirty_;
} }
void PlanView::save_dirty() { void PlanView::save_dirty() {

View File

@@ -30,6 +30,7 @@ public:
PlanView(Project& project, GrexConfig& grex_config); PlanView(Project& project, GrexConfig& grex_config);
GtkWidget* widget() { return root_; } GtkWidget* widget() { return root_; }
void refresh(); void refresh();
void reload_plan_from_disk();
bool is_dirty() const; bool is_dirty() const;
void save_dirty(); void save_dirty();
void revert_dirty(); void revert_dirty();

View File

@@ -417,7 +417,8 @@ void ShellsView::on_delete_file(GtkButton*, gpointer data) {
auto* self = static_cast<ShellsView*>(data); auto* self = static_cast<ShellsView*>(data);
if (!self->selected_file_) return; if (!self->selected_file_) return;
auto name = self->selected_file_->filepath.filename().string(); auto filepath = self->selected_file_->filepath;
auto name = filepath.filename().string();
auto it = std::find_if(self->project_.shell_files.begin(), self->project_.shell_files.end(), auto it = std::find_if(self->project_.shell_files.begin(), self->project_.shell_files.end(),
[&](const ShellsFile& sf) { return &sf == self->selected_file_; }); [&](const ShellsFile& sf) { return &sf == self->selected_file_; });
@@ -426,7 +427,13 @@ void ShellsView::on_delete_file(GtkButton*, gpointer data) {
self->selected_file_ = nullptr; self->selected_file_ = nullptr;
self->populate_file_list(); self->populate_file_list();
self->project_.report_status("Removed shell file from project: " + name + " (file not deleted from disk)");
std::error_code ec;
if (std::filesystem::remove(filepath, ec))
self->project_.report_status("Deleted shell file: " + name);
else
self->project_.report_status("Error: could not delete " + name +
(ec ? ": " + ec.message() : ""));
} }
void ShellsView::on_new_shell(GtkButton*, gpointer data) { void ShellsView::on_new_shell(GtkButton*, gpointer data) {

View File

@@ -191,9 +191,9 @@ void UnitEditor::revert_current() {
clear_dirty(); clear_dirty();
} }
void UnitEditor::set_name_changed_callback(NameChangedCallback cb, void* data) { void UnitEditor::set_unit_edited_callback(UnitEditedCallback cb, void* data) {
name_cb_ = cb; edit_cb_ = cb;
name_cb_data_ = data; edit_cb_data_ = data;
} }
void UnitEditor::on_edit_unit(GtkButton*, gpointer data) { void UnitEditor::on_edit_unit(GtkButton*, gpointer data) {
@@ -205,15 +205,17 @@ void UnitEditor::on_edit_unit(GtkButton*, gpointer data) {
self->project_, self->grex_config_, self->project_.all_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; auto new_name = self->current_unit_->name;
bool name_changed = (new_name != self->current_unit_name_);
self->current_unit_name_ = new_name; self->current_unit_name_ = new_name;
if (name_changed) {
if (self->current_task_) if (self->current_task_)
self->current_task_->name = new_name; self->current_task_->name = new_name;
gtk_label_set_text(GTK_LABEL(self->name_display_), new_name.c_str()); 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(); self->mark_dirty();
if (self->edit_cb_)
self->edit_cb_(new_name, name_changed, self->edit_cb_data_);
} }
} }
@@ -234,7 +236,7 @@ void UnitEditor::on_select_unit(GtkButton*, gpointer data) {
if (self->current_task_) self->current_task_->name = unit_name; if (self->current_task_) self->current_task_->name = unit_name;
Unit* unit = self->project_.find_unit(unit_name); Unit* unit = self->project_.find_unit(unit_name);
self->load(self->current_task_, unit); self->load(self->current_task_, unit);
if (self->name_cb_) self->name_cb_(unit_name, self->name_cb_data_); if (self->edit_cb_) self->edit_cb_(unit_name, true, self->edit_cb_data_);
}); });
} }

View File

@@ -41,8 +41,8 @@ public:
void save_current(); void save_current();
void revert_current(); void revert_current();
using NameChangedCallback = void(*)(const std::string& new_name, void* data); using UnitEditedCallback = void(*)(const std::string& new_name, bool name_changed, void* data);
void set_name_changed_callback(NameChangedCallback cb, void* data); void set_unit_edited_callback(UnitEditedCallback cb, void* data);
private: private:
Project& project_; Project& project_;
@@ -62,8 +62,8 @@ private:
GtkWidget* btn_save_unit_; GtkWidget* btn_save_unit_;
GtkWidget* entry_comment_; GtkWidget* entry_comment_;
NameChangedCallback name_cb_ = nullptr; UnitEditedCallback edit_cb_ = nullptr;
void* name_cb_data_ = nullptr; void* edit_cb_data_ = nullptr;
bool dirty_ = false; bool dirty_ = false;

View File

@@ -145,8 +145,14 @@ UnitsView::UnitsView(Project& project, GrexConfig& grex_config)
g_signal_connect(btn_move_up_, "clicked", G_CALLBACK(on_move_up), 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_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-activated", G_CALLBACK(on_unit_activated), this);
g_signal_connect(unit_listbox_, "row-selected", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow*, gpointer d) { g_signal_connect(unit_listbox_, "row-selected", G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer d) {
static_cast<UnitsView*>(d)->update_move_buttons(); auto* self = static_cast<UnitsView*>(d);
self->update_move_buttons();
if (row && self->selected_file_) {
int idx = gtk_list_box_row_get_index(row);
if (idx >= 0 && idx < (int)self->selected_file_->units.size())
self->project_.check_unit_valid(self->selected_file_->units[idx]);
}
}), this); }), this);
g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) { g_signal_connect(btn_refresh, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
@@ -231,8 +237,15 @@ void UnitsView::populate_unit_list() {
for (auto& u : selected_file_->units) { for (auto& u : selected_file_->units) {
auto* row = gtk_list_box_row_new(); auto* row = gtk_list_box_row_new();
auto text = std::string("\u2022 ") + u.name; auto* label = gtk_label_new(nullptr);
auto* label = gtk_label_new(text.c_str()); auto* escaped = g_markup_escape_text(u.name.c_str(), -1);
std::string markup;
if (project_.check_unit_valid(u))
markup = std::string("\u2022 ") + escaped;
else
markup = std::string("<span foreground=\"red\">\u2022 ") + escaped + "</span>";
g_free(escaped);
gtk_label_set_markup(GTK_LABEL(label), markup.c_str());
gtk_label_set_xalign(GTK_LABEL(label), 0.0f); gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
gtk_widget_set_margin_start(label, 8); gtk_widget_set_margin_start(label, 8);
gtk_widget_set_margin_end(label, 8); gtk_widget_set_margin_end(label, 8);
@@ -403,7 +416,8 @@ void UnitsView::on_delete_file(GtkButton*, gpointer data) {
auto* self = static_cast<UnitsView*>(data); auto* self = static_cast<UnitsView*>(data);
if (!self->selected_file_) return; if (!self->selected_file_) return;
auto name = self->selected_file_->filepath.filename().string(); auto filepath = self->selected_file_->filepath;
auto name = filepath.filename().string();
auto it = std::find_if(self->project_.unit_files.begin(), self->project_.unit_files.end(), auto it = std::find_if(self->project_.unit_files.begin(), self->project_.unit_files.end(),
[&](const UnitFile& uf) { return &uf == self->selected_file_; }); [&](const UnitFile& uf) { return &uf == self->selected_file_; });
@@ -412,7 +426,13 @@ void UnitsView::on_delete_file(GtkButton*, gpointer data) {
self->selected_file_ = nullptr; self->selected_file_ = nullptr;
self->populate_file_list(); self->populate_file_list();
self->project_.report_status("Removed unit file from project: " + name + " (file not deleted from disk)");
std::error_code ec;
if (std::filesystem::remove(filepath, ec))
self->project_.report_status("Deleted unit file: " + name);
else
self->project_.report_status("Error: could not delete " + name +
(ec ? ": " + ec.message() : ""));
} }
void UnitsView::on_new_unit(GtkButton*, gpointer data) { void UnitsView::on_new_unit(GtkButton*, gpointer data) {