Compare commits
2 Commits
219e316822
...
e55da16193
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e55da16193 | ||
|
|
e852b7e182 |
@@ -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,82 @@ fs::path Project::resolved_shells_path() const {
|
|||||||
return root / sp;
|
return root / sp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the file path portion before any arguments
|
||||||
|
static std::string extract_path(const std::string& s) {
|
||||||
|
auto pos = s.find(' ');
|
||||||
|
return pos == std::string::npos ? s : s.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
auto target_path = extract_path(u.target);
|
||||||
|
if (!fs::exists(target_path)) {
|
||||||
|
report_status("Unit '" + u.name + "': target does not exist: " + target_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (access(target_path.c_str(), X_OK) != 0) {
|
||||||
|
report_status("Unit '" + u.name + "': target is not executable: " + target_path);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
auto rectifier_path = extract_path(u.rectifier);
|
||||||
|
if (!fs::exists(rectifier_path)) {
|
||||||
|
report_status("Unit '" + u.name + "': rectifier does not exist: " + rectifier_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (access(rectifier_path.c_str(), X_OK) != 0) {
|
||||||
|
report_status("Unit '" + u.name + "': rectifier is not executable: " + rectifier_path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u.supply_environment) {
|
||||||
|
if (u.environment.empty()) {
|
||||||
|
report_status("Unit '" + u.name + "': environment file is not defined");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto env_path = extract_path(u.environment);
|
||||||
|
if (!fs::exists(env_path)) {
|
||||||
|
report_status("Unit '" + u.name + "': environment file does not exist: " + env_path);
|
||||||
|
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,116 @@ 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 std::string extract_path(const std::string& s) {
|
||||||
|
auto pos = s.find(' ');
|
||||||
|
return pos == std::string::npos ? s : s.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
auto tp = extract_path(u.target);
|
||||||
|
if (!fs::exists(tp)) {
|
||||||
|
valid = false;
|
||||||
|
if (error_msg.empty()) error_msg = "Target does not exist: " + tp;
|
||||||
|
} else if (access(tp.c_str(), X_OK) != 0) {
|
||||||
|
valid = false;
|
||||||
|
if (error_msg.empty()) error_msg = "Target is not executable: " + tp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
auto rp = extract_path(u.rectifier);
|
||||||
|
if (!fs::exists(rp)) {
|
||||||
|
valid = false;
|
||||||
|
if (error_msg.empty()) error_msg = "Rectifier does not exist: " + rp;
|
||||||
|
} else if (access(rp.c_str(), X_OK) != 0) {
|
||||||
|
valid = false;
|
||||||
|
if (error_msg.empty()) error_msg = "Rectifier is not executable: " + rp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
auto ep = extract_path(u.environment);
|
||||||
|
if (!fs::exists(ep)) {
|
||||||
|
valid = false;
|
||||||
|
if (error_msg.empty()) error_msg = "Environment file does not exist: " + ep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 +504,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 +513,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 +546,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);
|
||||||
|
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() {
|
||||||
|
|||||||
@@ -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 (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_);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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