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:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
self->plan_dirty_ = true;
|
if (name_changed) {
|
||||||
gtk_widget_add_css_class(self->btn_save_plan_, "suggested-action");
|
self->plan_dirty_ = true;
|
||||||
|
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::reload_plan_from_disk() {
|
||||||
|
auto* plan = current_plan();
|
||||||
|
if (!plan) return;
|
||||||
|
try {
|
||||||
|
*plan = Plan::load(plan->filepath);
|
||||||
|
project_.report_status("Reloaded plan: " + plan->filepath.filename().string());
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
project_.report_status("Error reloading plan: " + std::string(e.what()));
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
void PlanView::refresh() {
|
void PlanView::refresh() {
|
||||||
// reload units if paths now resolve
|
// reload units if paths now resolve
|
||||||
project_.load_all_units();
|
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)
|
||||||
try {
|
|
||||||
*plan = Plan::load(plan->filepath);
|
|
||||||
project_.report_status("Reloaded plan: " + plan->filepath.filename().string());
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
project_.report_status("Error reloading plan: " + std::string(e.what()));
|
|
||||||
}
|
|
||||||
gtk_label_set_markup(GTK_LABEL(plan_label_), (std::string("<b>Plan:</b> ") + plan->filepath.filename().string()).c_str());
|
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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 (self->current_task_)
|
if (name_changed) {
|
||||||
self->current_task_->name = new_name;
|
if (self->current_task_)
|
||||||
gtk_label_set_text(GTK_LABEL(self->name_display_), new_name.c_str());
|
self->current_task_->name = new_name;
|
||||||
if (self->name_cb_)
|
gtk_label_set_text(GTK_LABEL(self->name_display_), new_name.c_str());
|
||||||
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_);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user