Compare commits
3 Commits
219e316822
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36164c01a6 | ||
|
|
e55da16193 | ||
|
|
e852b7e182 |
14
src/grex.cpp
14
src/grex.cpp
@@ -19,10 +19,23 @@
|
|||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <cstring>
|
||||||
#include "models/project.h"
|
#include "models/project.h"
|
||||||
#include "models/grex_config.h"
|
#include "models/grex_config.h"
|
||||||
#include "views/main_window.h"
|
#include "views/main_window.h"
|
||||||
|
|
||||||
|
// Suppress known GTK4 bug: GtkDropDown's internal arrow GtkImage reports
|
||||||
|
// INT_MIN baselines instead of -1, triggering a spurious warning.
|
||||||
|
static GLogWriterOutput grex_log_writer(GLogLevelFlags level, const GLogField* fields,
|
||||||
|
gsize n_fields, gpointer) {
|
||||||
|
for (gsize i = 0; i < n_fields; i++) {
|
||||||
|
if (std::strcmp(fields[i].key, "MESSAGE") == 0 &&
|
||||||
|
std::strstr(static_cast<const char*>(fields[i].value), "reported baselines") != nullptr)
|
||||||
|
return G_LOG_WRITER_HANDLED;
|
||||||
|
}
|
||||||
|
return g_log_writer_default(level, fields, n_fields, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
static grex::Project* g_project = nullptr;
|
static grex::Project* g_project = nullptr;
|
||||||
static grex::GrexConfig* g_grex_config = nullptr;
|
static grex::GrexConfig* g_grex_config = nullptr;
|
||||||
static grex::MainWindow* g_main_window = nullptr;
|
static grex::MainWindow* g_main_window = nullptr;
|
||||||
@@ -65,6 +78,7 @@ static void on_activate(GtkApplication* app, gpointer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
|
g_log_set_writer_func(grex_log_writer, nullptr, nullptr);
|
||||||
auto* app = gtk_application_new("org.darkhorselinux.grex", G_APPLICATION_HANDLES_COMMAND_LINE);
|
auto* app = gtk_application_new("org.darkhorselinux.grex", G_APPLICATION_HANDLES_COMMAND_LINE);
|
||||||
|
|
||||||
GOptionEntry entries[] = {
|
GOptionEntry entries[] = {
|
||||||
|
|||||||
@@ -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,22 @@ struct DialogState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static void update_sensitivity(DialogState* s);
|
static void update_sensitivity(DialogState* s);
|
||||||
|
static void validate_fields(DialogState* s);
|
||||||
|
|
||||||
|
static std::string extract_path(const std::string& s) {
|
||||||
|
auto pos = s.find(' ');
|
||||||
|
return pos == std::string::npos ? s : s.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -273,7 +282,7 @@ static GtkWidget* make_file_row(DialogState* s, GtkWidget* grid, int row, const
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
fs::path p(raw);
|
fs::path p(extract_path(raw));
|
||||||
if (p.is_relative()) {
|
if (p.is_relative()) {
|
||||||
auto root = fbd->state->project->resolved_project_root();
|
auto root = fbd->state->project->resolved_project_root();
|
||||||
if (!root.empty())
|
if (!root.empty())
|
||||||
@@ -361,6 +370,111 @@ 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 {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,12 @@
|
|||||||
#include "views/config_view.h"
|
#include "views/config_view.h"
|
||||||
#include "util/unsaved_dialog.h"
|
#include "util/unsaved_dialog.h"
|
||||||
#include "util/json_helpers.h"
|
#include "util/json_helpers.h"
|
||||||
|
#include <cerrno>
|
||||||
|
#include <climits>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
namespace grex {
|
namespace grex {
|
||||||
|
|
||||||
@@ -123,6 +127,124 @@ ConfigView::ConfigView(Project& project) : project_(project) {
|
|||||||
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 16);
|
gtk_grid_set_column_spacing(GTK_GRID(vars_grid_), 16);
|
||||||
gtk_box_append(GTK_BOX(vars_box), vars_grid_);
|
gtk_box_append(GTK_BOX(vars_box), vars_grid_);
|
||||||
|
|
||||||
|
// --- Current Working Directory subsection ---
|
||||||
|
gtk_box_append(GTK_BOX(vars_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||||
|
|
||||||
|
auto* cwd_label = gtk_label_new(nullptr);
|
||||||
|
gtk_label_set_markup(GTK_LABEL(cwd_label), "<b>Current Working Directory</b>");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(cwd_label), 0.0f);
|
||||||
|
gtk_box_append(GTK_BOX(vars_box), cwd_label);
|
||||||
|
|
||||||
|
auto* cwd_desc = gtk_label_new("Set the working directory for this session. Relative paths in unit validation resolve against this.");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(cwd_desc), 0.0f);
|
||||||
|
gtk_label_set_wrap(GTK_LABEL(cwd_desc), TRUE);
|
||||||
|
gtk_widget_add_css_class(cwd_desc, "dim-label");
|
||||||
|
gtk_box_append(GTK_BOX(vars_box), cwd_desc);
|
||||||
|
|
||||||
|
auto* cwd_grid = gtk_grid_new();
|
||||||
|
gtk_grid_set_row_spacing(GTK_GRID(cwd_grid), 8);
|
||||||
|
gtk_grid_set_column_spacing(GTK_GRID(cwd_grid), 12);
|
||||||
|
|
||||||
|
auto* cwd_key = gtk_label_new("Directory");
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(cwd_key), 1.0f);
|
||||||
|
gtk_widget_set_size_request(cwd_key, 140, -1);
|
||||||
|
gtk_widget_add_css_class(cwd_key, "dim-label");
|
||||||
|
gtk_grid_attach(GTK_GRID(cwd_grid), cwd_key, 0, 0, 1, 1);
|
||||||
|
|
||||||
|
auto* cwd_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||||
|
gtk_widget_set_hexpand(cwd_hbox, TRUE);
|
||||||
|
|
||||||
|
char cwd_buf[PATH_MAX];
|
||||||
|
const char* initial_cwd = getcwd(cwd_buf, sizeof(cwd_buf));
|
||||||
|
cwd_entry_ = gtk_entry_new();
|
||||||
|
gtk_widget_set_hexpand(cwd_entry_, TRUE);
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(cwd_entry_), initial_cwd ? initial_cwd : "");
|
||||||
|
gtk_box_append(GTK_BOX(cwd_hbox), cwd_entry_);
|
||||||
|
|
||||||
|
auto* cwd_browse = gtk_button_new_with_label("Browse...");
|
||||||
|
gtk_box_append(GTK_BOX(cwd_hbox), cwd_browse);
|
||||||
|
|
||||||
|
auto* cwd_apply = gtk_button_new_with_label("Apply");
|
||||||
|
gtk_box_append(GTK_BOX(cwd_hbox), cwd_apply);
|
||||||
|
|
||||||
|
gtk_grid_attach(GTK_GRID(cwd_grid), cwd_hbox, 1, 0, 1, 1);
|
||||||
|
|
||||||
|
cwd_status_ = gtk_label_new(nullptr);
|
||||||
|
gtk_label_set_xalign(GTK_LABEL(cwd_status_), 0.0f);
|
||||||
|
gtk_widget_add_css_class(cwd_status_, "dim-label");
|
||||||
|
gtk_grid_attach(GTK_GRID(cwd_grid), cwd_status_, 1, 1, 1, 1);
|
||||||
|
|
||||||
|
gtk_box_append(GTK_BOX(vars_box), cwd_grid);
|
||||||
|
|
||||||
|
// Apply button — chdir and report
|
||||||
|
g_signal_connect(cwd_apply, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||||
|
auto* self = static_cast<ConfigView*>(d);
|
||||||
|
auto dir = std::string(gtk_editable_get_text(GTK_EDITABLE(self->cwd_entry_)));
|
||||||
|
if (dir.empty()) {
|
||||||
|
self->project_.report_status("Error: working directory is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (chdir(dir.c_str()) == 0) {
|
||||||
|
char buf[PATH_MAX];
|
||||||
|
const char* actual = getcwd(buf, sizeof(buf));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(self->cwd_entry_), actual ? actual : dir.c_str());
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->cwd_status_), "");
|
||||||
|
self->project_.report_status("Working directory set to: " + std::string(actual ? actual : dir.c_str()));
|
||||||
|
self->update_resolved_labels();
|
||||||
|
} else {
|
||||||
|
auto msg = "Error: cannot chdir to '" + dir + "': " + std::string(strerror(errno));
|
||||||
|
self->project_.report_status(msg);
|
||||||
|
gtk_label_set_markup(GTK_LABEL(self->cwd_status_),
|
||||||
|
("<span foreground=\"#cc0000\">" + msg + "</span>").c_str());
|
||||||
|
}
|
||||||
|
}), this);
|
||||||
|
|
||||||
|
// Browse button — folder chooser
|
||||||
|
g_signal_connect(cwd_browse, "clicked", G_CALLBACK(+[](GtkButton*, gpointer d) {
|
||||||
|
auto* self = static_cast<ConfigView*>(d);
|
||||||
|
auto* window = GTK_WINDOW(gtk_widget_get_ancestor(self->root_, GTK_TYPE_WINDOW));
|
||||||
|
auto* dialog = gtk_file_dialog_new();
|
||||||
|
gtk_file_dialog_set_title(dialog, "Select Working Directory");
|
||||||
|
|
||||||
|
struct CwdBrowseCtx { ConfigView* view; };
|
||||||
|
auto* ctx = new CwdBrowseCtx{self};
|
||||||
|
gtk_file_dialog_select_folder(dialog, window, nullptr,
|
||||||
|
+[](GObject* source, GAsyncResult* res, gpointer data) {
|
||||||
|
auto* ctx = static_cast<CwdBrowseCtx*>(data);
|
||||||
|
GError* error = nullptr;
|
||||||
|
auto* file = gtk_file_dialog_select_folder_finish(GTK_FILE_DIALOG(source), res, &error);
|
||||||
|
if (file) {
|
||||||
|
auto* path = g_file_get_path(file);
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(ctx->view->cwd_entry_), path);
|
||||||
|
g_free(path);
|
||||||
|
g_object_unref(file);
|
||||||
|
} else if (error) {
|
||||||
|
g_error_free(error);
|
||||||
|
}
|
||||||
|
delete ctx;
|
||||||
|
}, ctx);
|
||||||
|
}), this);
|
||||||
|
|
||||||
|
// Enter in entry triggers apply
|
||||||
|
g_signal_connect(cwd_entry_, "activate", G_CALLBACK(+[](GtkEntry*, gpointer d) {
|
||||||
|
auto* self = static_cast<ConfigView*>(d);
|
||||||
|
auto dir = std::string(gtk_editable_get_text(GTK_EDITABLE(self->cwd_entry_)));
|
||||||
|
if (dir.empty()) return;
|
||||||
|
if (chdir(dir.c_str()) == 0) {
|
||||||
|
char buf[PATH_MAX];
|
||||||
|
const char* actual = getcwd(buf, sizeof(buf));
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(self->cwd_entry_), actual ? actual : dir.c_str());
|
||||||
|
gtk_label_set_text(GTK_LABEL(self->cwd_status_), "");
|
||||||
|
self->project_.report_status("Working directory set to: " + std::string(actual ? actual : dir.c_str()));
|
||||||
|
self->update_resolved_labels();
|
||||||
|
} else {
|
||||||
|
auto msg = "Error: cannot chdir to '" + dir + "': " + std::string(strerror(errno));
|
||||||
|
self->project_.report_status(msg);
|
||||||
|
gtk_label_set_markup(GTK_LABEL(self->cwd_status_),
|
||||||
|
("<span foreground=\"#cc0000\">" + msg + "</span>").c_str());
|
||||||
|
}
|
||||||
|
}), this);
|
||||||
|
|
||||||
gtk_frame_set_child(GTK_FRAME(vars_frame), vars_box);
|
gtk_frame_set_child(GTK_FRAME(vars_frame), vars_box);
|
||||||
gtk_box_append(GTK_BOX(config_content_), vars_frame);
|
gtk_box_append(GTK_BOX(config_content_), vars_frame);
|
||||||
|
|
||||||
@@ -297,14 +419,12 @@ void ConfigView::update_resolved_labels() {
|
|||||||
// Status indicator
|
// Status indicator
|
||||||
bool path_ok = (display != "(unresolved)") &&
|
bool path_ok = (display != "(unresolved)") &&
|
||||||
(std::filesystem::exists(display) || std::filesystem::is_directory(display));
|
(std::filesystem::exists(display) || std::filesystem::is_directory(display));
|
||||||
auto* indicator = gtk_label_new(nullptr);
|
auto* indicator = gtk_label_new(path_ok ? "OK" : "ERR");
|
||||||
gtk_label_set_xalign(GTK_LABEL(indicator), 0.5f);
|
gtk_label_set_xalign(GTK_LABEL(indicator), 0.5f);
|
||||||
if (display == "(unresolved)")
|
if (path_ok)
|
||||||
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#cc0000\">\u2718</span>");
|
gtk_widget_add_css_class(indicator, "success");
|
||||||
else if (path_ok)
|
|
||||||
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#4e9a06\">\u2714</span>");
|
|
||||||
else
|
else
|
||||||
gtk_label_set_markup(GTK_LABEL(indicator), "<span foreground=\"#cc0000\">\u2718</span>");
|
gtk_widget_add_css_class(indicator, "error");
|
||||||
gtk_grid_attach(GTK_GRID(resolved_grid_), indicator, 1, row, 1, 1);
|
gtk_grid_attach(GTK_GRID(resolved_grid_), indicator, 1, row, 1, 1);
|
||||||
|
|
||||||
auto* val_label = gtk_label_new(nullptr);
|
auto* val_label = gtk_label_new(nullptr);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
#include "models/project.h"
|
#include "models/project.h"
|
||||||
|
|
||||||
namespace grex {
|
namespace grex {
|
||||||
@@ -58,6 +59,8 @@ private:
|
|||||||
GtkWidget* btn_close_;
|
GtkWidget* btn_close_;
|
||||||
GtkWidget* btn_save_;
|
GtkWidget* btn_save_;
|
||||||
GtkWidget* config_content_;
|
GtkWidget* config_content_;
|
||||||
|
GtkWidget* cwd_entry_;
|
||||||
|
GtkWidget* cwd_status_;
|
||||||
|
|
||||||
void build_config_fields();
|
void build_config_fields();
|
||||||
void build_variables_section();
|
void build_variables_section();
|
||||||
|
|||||||
@@ -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