merge: issue/0007-type-editor-panel — Type Editor panel
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
---
|
||||
id: 0007
|
||||
title: Type Editor panel — CRUD de tipos desde la app
|
||||
status: pending
|
||||
status: completed
|
||||
priority: medium
|
||||
created: 2026-04-30
|
||||
completed: 2026-05-01
|
||||
depends_on: [0005, 0006]
|
||||
---
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
#include "entity_ops.h"
|
||||
#include "project_manager.h"
|
||||
|
||||
#include "../../../../cpp/vendor/sqlite3/sqlite3.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -206,6 +208,10 @@ static bool load_input() {
|
||||
g_app.parsed_types = std::move(pt);
|
||||
}
|
||||
}
|
||||
// Inicializar el draft del Type Editor con copia de parsed_types (0007).
|
||||
g_app.types_draft = g_app.parsed_types;
|
||||
g_app.types_dirty = false;
|
||||
g_app.types_save_error.clear();
|
||||
|
||||
// Restablecer viewport state (preserva camara user-visible)
|
||||
g_viewport.selection.clear();
|
||||
@@ -444,6 +450,7 @@ static fn_ui::PanelToggle g_panels[] = {
|
||||
{"Inspector", nullptr, &g_app.panel_inspector},
|
||||
{"Stats", nullptr, &g_app.panel_stats},
|
||||
{"Note", nullptr, &g_app.panel_note},
|
||||
{"Types", nullptr, &g_app.panel_type_editor},
|
||||
};
|
||||
|
||||
static void render() {
|
||||
@@ -573,6 +580,86 @@ static void render() {
|
||||
load_input();
|
||||
}
|
||||
|
||||
// ---- Type Editor (issue 0007) ----
|
||||
if (g_app.want_types_save) {
|
||||
g_app.want_types_save = false;
|
||||
g_app.types_save_error.clear();
|
||||
if (g_types_path.empty()) {
|
||||
g_app.types_save_error =
|
||||
"No hay types.yaml asignado (abre un proyecto o usa --types).";
|
||||
} else {
|
||||
std::string err;
|
||||
if (!ge::types_save_yaml(g_types_path.c_str(),
|
||||
g_app.types_draft, &err)) {
|
||||
g_app.types_save_error = "Save failed: " + err;
|
||||
} else {
|
||||
std::fprintf(stdout,
|
||||
"[graph_explorer] types.yaml saved -> %s\n",
|
||||
g_types_path.c_str());
|
||||
g_app.parsed_types = g_app.types_draft;
|
||||
std::vector<uint16_t> cps =
|
||||
ge::apply_types_yaml(g_graph, g_app.parsed_types);
|
||||
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
|
||||
g_atlas = ge::build_icon_atlas(cps);
|
||||
g_atlas_bound = false;
|
||||
g_gpu_dirty = true;
|
||||
g_app.types_dirty = false;
|
||||
ge::views_inspector_refresh_caches(g_app);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (g_app.want_types_reload) {
|
||||
g_app.want_types_reload = false;
|
||||
g_app.types_save_error.clear();
|
||||
if (g_types_path.empty()) {
|
||||
// Sin types.yaml en disco: descarta el draft a parsed_types actual.
|
||||
g_app.types_draft = g_app.parsed_types;
|
||||
g_app.types_dirty = false;
|
||||
} else {
|
||||
ge::ParsedTypes pt;
|
||||
std::string err;
|
||||
if (!ge::types_load_yaml(g_types_path.c_str(), &pt, &err)) {
|
||||
g_app.types_save_error = "Reload failed: " + err;
|
||||
} else {
|
||||
std::vector<uint16_t> cps = ge::apply_types_yaml(g_graph, pt);
|
||||
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
|
||||
g_atlas = ge::build_icon_atlas(cps);
|
||||
g_atlas_bound = false;
|
||||
g_gpu_dirty = true;
|
||||
g_app.parsed_types = pt;
|
||||
g_app.types_draft = std::move(pt);
|
||||
g_app.types_dirty = false;
|
||||
ge::views_inspector_refresh_caches(g_app);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Conteo de uso para el modal de borrado (entidades activas en BD).
|
||||
if (g_app.show_te_delete_modal && g_app.te_delete_use_count == 0
|
||||
&& !g_app.input_db_path.empty()) {
|
||||
const char* tname = nullptr;
|
||||
if (g_app.te_pending_delete_e >= 0
|
||||
&& g_app.te_pending_delete_e < (int)g_app.types_draft.entities.size()) {
|
||||
tname = g_app.types_draft.entities[g_app.te_pending_delete_e].name.c_str();
|
||||
}
|
||||
if (tname && *tname) {
|
||||
sqlite3* db = nullptr;
|
||||
if (sqlite3_open_v2(g_app.input_db_path.c_str(), &db,
|
||||
SQLITE_OPEN_READONLY, nullptr) == SQLITE_OK) {
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(db,
|
||||
"SELECT COUNT(*) FROM entities WHERE type_ref = ?",
|
||||
-1, &st, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, tname, -1, SQLITE_TRANSIENT);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
g_app.te_delete_use_count = sqlite3_column_int(st, 0);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
sqlite3_close(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Mutaciones (add/delete/duplicate/change_type) ----
|
||||
auto reload_after_mutation = [&]() {
|
||||
graph::GraphLoadStats stats{};
|
||||
@@ -845,6 +932,13 @@ static void render() {
|
||||
ImGui::SetNextWindowSize(ImVec2(700.0f, 480.0f), ImGuiCond_FirstUseEver);
|
||||
ge::views_note(g_app);
|
||||
|
||||
// Type Editor (issue 0007) — flotante, dockeable.
|
||||
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.20f, top + 40.0f),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(720.0f, 500.0f), ImGuiCond_FirstUseEver);
|
||||
ge::views_type_editor(g_app);
|
||||
ge::views_type_editor_delete_modal(g_app);
|
||||
|
||||
g_first_render = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -410,6 +410,18 @@ const char* style_name(uint8_t s) {
|
||||
}
|
||||
}
|
||||
|
||||
const char* shape_name(uint8_t s) {
|
||||
switch (s) {
|
||||
case SHAPE_CIRCLE: return "circle";
|
||||
case SHAPE_SQUARE: return "square";
|
||||
case SHAPE_DIAMOND: return "diamond";
|
||||
case SHAPE_HEX: return "hex";
|
||||
case SHAPE_TRIANGLE: return "triangle";
|
||||
case SHAPE_ROUNDED_SQUARE: return "rounded_square";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Serializa un FieldSpec como inline-map: "{ name: X, type: Y, required: true, values: [a,b,c] }".
|
||||
std::string serialize_field(const FieldSpec& f) {
|
||||
std::string out = "{ name: ";
|
||||
@@ -454,6 +466,8 @@ bool types_save_yaml(const char* path, const ParsedTypes& types,
|
||||
f << " - name: " << e.name << "\n";
|
||||
std::string col = color_to_hex(e.color);
|
||||
if (!col.empty()) f << " color: \"" << col << "\"\n";
|
||||
const char* sh = shape_name(e.shape);
|
||||
if (sh && *sh) f << " shape: " << sh << "\n";
|
||||
if (!e.icon_name.empty()) f << " icon: " << e.icon_name << "\n";
|
||||
if (!e.principal_field.empty())
|
||||
f << " principal_field: " << e.principal_field << "\n";
|
||||
|
||||
@@ -1391,4 +1391,413 @@ bool views_open_modal(AppState& app) {
|
||||
return opened;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Type Editor (issue 0007)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
const char* k_shape_names[] = {
|
||||
"(use type)", "circle", "square", "diamond", "hex", "triangle", "rounded_square",
|
||||
};
|
||||
constexpr int k_shape_count = (int)(sizeof(k_shape_names) / sizeof(k_shape_names[0]));
|
||||
|
||||
const char* k_style_names[] = {
|
||||
"(use type)", "solid", "dashed", "dotted",
|
||||
};
|
||||
constexpr int k_style_count = (int)(sizeof(k_style_names) / sizeof(k_style_names[0]));
|
||||
|
||||
const char* k_field_kind_names[] = {
|
||||
"string", "int", "float", "bool", "date", "url", "enum",
|
||||
};
|
||||
constexpr int k_field_kind_count = (int)(sizeof(k_field_kind_names) / sizeof(k_field_kind_names[0]));
|
||||
|
||||
ImVec4 abgr_to_imvec4_full(uint32_t c) {
|
||||
return ImVec4(
|
||||
(float)( c & 0xFF) / 255.0f,
|
||||
(float)((c >> 8) & 0xFF) / 255.0f,
|
||||
(float)((c >> 16) & 0xFF) / 255.0f,
|
||||
(float)((c >> 24) & 0xFF) / 255.0f);
|
||||
}
|
||||
|
||||
uint32_t imvec4_to_abgr(const ImVec4& v) {
|
||||
auto clamp01 = [](float x) { return x < 0 ? 0.f : (x > 1 ? 1.f : x); };
|
||||
uint8_t r = (uint8_t)(clamp01(v.x) * 255.0f + 0.5f);
|
||||
uint8_t g = (uint8_t)(clamp01(v.y) * 255.0f + 0.5f);
|
||||
uint8_t b = (uint8_t)(clamp01(v.z) * 255.0f + 0.5f);
|
||||
uint8_t a = (uint8_t)(clamp01(v.w) * 255.0f + 0.5f);
|
||||
return (uint32_t)r | ((uint32_t)g << 8) | ((uint32_t)b << 16) | ((uint32_t)a << 24);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void views_type_editor(AppState& app) {
|
||||
if (!app.panel_type_editor) return;
|
||||
if (!ImGui::Begin("Types", &app.panel_type_editor)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("##te_tabs")) {
|
||||
// ---- Entities tab --------------------------------------------------
|
||||
if (ImGui::BeginTabItem("Entities")) {
|
||||
app.te_tab_idx = 0;
|
||||
ImGui::BeginChild("##te_left", ImVec2(160, 0), true);
|
||||
for (int i = 0; i < (int)app.types_draft.entities.size(); ++i) {
|
||||
ImGui::PushID(i);
|
||||
bool sel = (i == app.te_entity_idx);
|
||||
const auto& e = app.types_draft.entities[i];
|
||||
if (ImGui::Selectable(e.name.empty() ? "(unnamed)" : e.name.c_str(), sel)) {
|
||||
app.te_entity_idx = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginGroup();
|
||||
// +/- buttons
|
||||
if (fn_ui::button(TI_PLUS " Add entity type", fn_ui::ButtonVariant::Subtle)) {
|
||||
EntitySpec ne;
|
||||
ne.name = "NewType";
|
||||
app.types_draft.entities.push_back(std::move(ne));
|
||||
app.te_entity_idx = (int)app.types_draft.entities.size() - 1;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool can_del = (app.te_entity_idx >= 0
|
||||
&& app.te_entity_idx < (int)app.types_draft.entities.size());
|
||||
if (!can_del) ImGui::BeginDisabled();
|
||||
if (fn_ui::button(TI_X " Delete", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.te_pending_delete_e = app.te_entity_idx;
|
||||
app.te_pending_delete_r = -1;
|
||||
app.show_te_delete_modal = true;
|
||||
}
|
||||
if (!can_del) ImGui::EndDisabled();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (can_del) {
|
||||
EntitySpec& e = app.types_draft.entities[app.te_entity_idx];
|
||||
|
||||
// Name
|
||||
char namebuf[80];
|
||||
std::snprintf(namebuf, sizeof(namebuf), "%s", e.name.c_str());
|
||||
if (ImGui::InputText("Name", namebuf, sizeof(namebuf))) {
|
||||
e.name = namebuf;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
|
||||
// Color
|
||||
ImVec4 col = abgr_to_imvec4_full(e.color ? e.color : 0xFF888888u);
|
||||
if (ImGui::ColorEdit4("Color", (float*)&col,
|
||||
ImGuiColorEditFlags_NoInputs
|
||||
| ImGuiColorEditFlags_AlphaBar)) {
|
||||
e.color = imvec4_to_abgr(col);
|
||||
app.types_dirty = true;
|
||||
}
|
||||
|
||||
// Shape
|
||||
int sh_idx = (e.shape <= SHAPE_ROUNDED_SQUARE) ? (int)e.shape : 0;
|
||||
if (ImGui::Combo("Shape", &sh_idx, k_shape_names, k_shape_count)) {
|
||||
e.shape = (uint8_t)sh_idx;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
|
||||
// Icon
|
||||
char ibuf[64];
|
||||
std::snprintf(ibuf, sizeof(ibuf), "%s", e.icon_name.c_str());
|
||||
if (ImGui::InputText("Icon (ti-*)", ibuf, sizeof(ibuf))) {
|
||||
e.icon_name = ibuf;
|
||||
e.icon_cp = tabler_codepoint_by_name(ibuf);
|
||||
app.types_dirty = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("cp=0x%04X", e.icon_cp);
|
||||
|
||||
// Principal field (combo entre fields existentes; permite "name" por default)
|
||||
std::vector<const char*> pf_opts;
|
||||
pf_opts.push_back("(name)");
|
||||
for (const auto& fs : e.fields) pf_opts.push_back(fs.name.c_str());
|
||||
int pf_idx = 0;
|
||||
for (int i = 1; i < (int)pf_opts.size(); ++i) {
|
||||
if (e.principal_field == pf_opts[i]) { pf_idx = i; break; }
|
||||
}
|
||||
if (e.principal_field.empty()) pf_idx = 0;
|
||||
if (ImGui::Combo("Principal field", &pf_idx,
|
||||
pf_opts.data(), (int)pf_opts.size())) {
|
||||
e.principal_field = (pf_idx == 0) ? std::string()
|
||||
: std::string(pf_opts[pf_idx]);
|
||||
app.types_dirty = true;
|
||||
}
|
||||
|
||||
// Fields table
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Fields");
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::BeginTable("##te_fields", 5,
|
||||
ImGuiTableFlags_BordersInnerV
|
||||
| ImGuiTableFlags_RowBg
|
||||
| ImGuiTableFlags_SizingStretchProp)) {
|
||||
ImGui::TableSetupColumn("name");
|
||||
ImGui::TableSetupColumn("type", ImGuiTableColumnFlags_WidthFixed, 90.0f);
|
||||
ImGui::TableSetupColumn("required", ImGuiTableColumnFlags_WidthFixed, 70.0f);
|
||||
ImGui::TableSetupColumn("values", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
int del_idx = -1;
|
||||
int up_idx = -1;
|
||||
int dn_idx = -1;
|
||||
for (int fi = 0; fi < (int)e.fields.size(); ++fi) {
|
||||
FieldSpec& fs = e.fields[fi];
|
||||
ImGui::PushID(fi);
|
||||
ImGui::TableNextRow();
|
||||
|
||||
// name
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
char fbuf[64];
|
||||
std::snprintf(fbuf, sizeof(fbuf), "%s", fs.name.c_str());
|
||||
if (ImGui::InputText("##fname", fbuf, sizeof(fbuf))) {
|
||||
fs.name = fbuf;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
|
||||
// type
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
int kidx = (int)fs.kind;
|
||||
if (kidx < 0 || kidx >= k_field_kind_count) kidx = 0;
|
||||
if (ImGui::Combo("##ftype", &kidx,
|
||||
k_field_kind_names, k_field_kind_count)) {
|
||||
fs.kind = (FieldKind)kidx;
|
||||
if (fs.kind != FK_ENUM) fs.enum_values.clear();
|
||||
app.types_dirty = true;
|
||||
}
|
||||
|
||||
// required
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
if (ImGui::Checkbox("##freq", &fs.required)) app.types_dirty = true;
|
||||
|
||||
// values (CSV editable solo si enum)
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
if (fs.kind == FK_ENUM) {
|
||||
std::string csv;
|
||||
for (size_t i = 0; i < fs.enum_values.size(); ++i) {
|
||||
if (i) csv += ", ";
|
||||
csv += fs.enum_values[i];
|
||||
}
|
||||
char vbuf[256];
|
||||
std::snprintf(vbuf, sizeof(vbuf), "%s", csv.c_str());
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::InputText("##fval", vbuf, sizeof(vbuf))) {
|
||||
fs.enum_values.clear();
|
||||
std::string s = vbuf;
|
||||
size_t start = 0;
|
||||
while (start < s.size()) {
|
||||
size_t end = s.find(',', start);
|
||||
if (end == std::string::npos) end = s.size();
|
||||
std::string tok = s.substr(start, end - start);
|
||||
while (!tok.empty() && std::isspace((unsigned char)tok.front())) tok.erase(tok.begin());
|
||||
while (!tok.empty() && std::isspace((unsigned char)tok.back())) tok.pop_back();
|
||||
if (!tok.empty()) fs.enum_values.push_back(std::move(tok));
|
||||
start = end + 1;
|
||||
}
|
||||
app.types_dirty = true;
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("-");
|
||||
}
|
||||
|
||||
// controls
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
if (ImGui::SmallButton(TI_X "##fd")) del_idx = fi;
|
||||
ImGui::SameLine();
|
||||
if (fi > 0 && ImGui::SmallButton("^##fu")) up_idx = fi;
|
||||
ImGui::SameLine();
|
||||
if (fi + 1 < (int)e.fields.size()
|
||||
&& ImGui::SmallButton("v##fdn")) dn_idx = fi;
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
|
||||
if (del_idx >= 0) {
|
||||
e.fields.erase(e.fields.begin() + del_idx);
|
||||
app.types_dirty = true;
|
||||
} else if (up_idx > 0) {
|
||||
std::swap(e.fields[up_idx - 1], e.fields[up_idx]);
|
||||
app.types_dirty = true;
|
||||
} else if (dn_idx >= 0 && dn_idx + 1 < (int)e.fields.size()) {
|
||||
std::swap(e.fields[dn_idx], e.fields[dn_idx + 1]);
|
||||
app.types_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (fn_ui::button(TI_PLUS " Add field", fn_ui::ButtonVariant::Subtle)) {
|
||||
FieldSpec ns;
|
||||
ns.name = "field" + std::to_string(e.fields.size() + 1);
|
||||
e.fields.push_back(std::move(ns));
|
||||
app.types_dirty = true;
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("(no entity selected)");
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
// ---- Relations tab -------------------------------------------------
|
||||
if (ImGui::BeginTabItem("Relations")) {
|
||||
app.te_tab_idx = 1;
|
||||
ImGui::BeginChild("##te_left_r", ImVec2(160, 0), true);
|
||||
for (int i = 0; i < (int)app.types_draft.relations.size(); ++i) {
|
||||
ImGui::PushID(i + 10000);
|
||||
bool sel = (i == app.te_relation_idx);
|
||||
const auto& r = app.types_draft.relations[i];
|
||||
if (ImGui::Selectable(r.name.empty() ? "(unnamed)" : r.name.c_str(), sel)) {
|
||||
app.te_relation_idx = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::BeginGroup();
|
||||
if (fn_ui::button(TI_PLUS " Add relation type", fn_ui::ButtonVariant::Subtle)) {
|
||||
RelationSpec nr;
|
||||
nr.name = "new_relation";
|
||||
app.types_draft.relations.push_back(std::move(nr));
|
||||
app.te_relation_idx = (int)app.types_draft.relations.size() - 1;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool can_del_r = (app.te_relation_idx >= 0
|
||||
&& app.te_relation_idx < (int)app.types_draft.relations.size());
|
||||
if (!can_del_r) ImGui::BeginDisabled();
|
||||
if (fn_ui::button(TI_X " Delete", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.te_pending_delete_r = app.te_relation_idx;
|
||||
app.te_pending_delete_e = -1;
|
||||
app.show_te_delete_modal = true;
|
||||
}
|
||||
if (!can_del_r) ImGui::EndDisabled();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (can_del_r) {
|
||||
RelationSpec& r = app.types_draft.relations[app.te_relation_idx];
|
||||
|
||||
char rnbuf[80];
|
||||
std::snprintf(rnbuf, sizeof(rnbuf), "%s", r.name.c_str());
|
||||
if (ImGui::InputText("Name", rnbuf, sizeof(rnbuf))) {
|
||||
r.name = rnbuf;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
ImVec4 rcol = abgr_to_imvec4_full(r.color ? r.color : 0xFF888888u);
|
||||
if (ImGui::ColorEdit4("Color", (float*)&rcol,
|
||||
ImGuiColorEditFlags_NoInputs
|
||||
| ImGuiColorEditFlags_AlphaBar)) {
|
||||
r.color = imvec4_to_abgr(rcol);
|
||||
app.types_dirty = true;
|
||||
}
|
||||
int st_idx = (r.style <= EDGE_DOTTED) ? (int)r.style : 0;
|
||||
if (ImGui::Combo("Style", &st_idx, k_style_names, k_style_count)) {
|
||||
r.style = (uint8_t)st_idx;
|
||||
app.types_dirty = true;
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled("(no relation selected)");
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
// ---- Footer (Save / Reload) -------------------------------------------
|
||||
ImGui::Separator();
|
||||
if (!app.types_dirty) ImGui::BeginDisabled();
|
||||
if (fn_ui::button(TI_DEVICE_FLOPPY " Save to types.yaml",
|
||||
fn_ui::ButtonVariant::Primary)) {
|
||||
app.want_types_save = true;
|
||||
}
|
||||
if (!app.types_dirty) ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button(TI_REFRESH " Reload from disk", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.want_types_reload = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (app.types_dirty) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.2f, 1.0f), "* unsaved changes");
|
||||
} else {
|
||||
ImGui::TextDisabled("clean");
|
||||
}
|
||||
if (!app.types_save_error.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
|
||||
app.types_save_error.c_str());
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
bool views_type_editor_delete_modal(AppState& app) {
|
||||
if (!app.show_te_delete_modal) return false;
|
||||
bool confirmed = false;
|
||||
if (fn_ui::modal_dialog_begin("Delete type", &app.show_te_delete_modal,
|
||||
ImVec2(440, 0))) {
|
||||
const char* tname = "?";
|
||||
const char* tkind = "?";
|
||||
if (app.te_pending_delete_e >= 0
|
||||
&& app.te_pending_delete_e < (int)app.types_draft.entities.size()) {
|
||||
tname = app.types_draft.entities[app.te_pending_delete_e].name.c_str();
|
||||
tkind = "entity";
|
||||
} else if (app.te_pending_delete_r >= 0
|
||||
&& app.te_pending_delete_r < (int)app.types_draft.relations.size()) {
|
||||
tname = app.types_draft.relations[app.te_pending_delete_r].name.c_str();
|
||||
tkind = "relation";
|
||||
}
|
||||
ImGui::Text("Eliminar %s type \"%s\"?", tkind, tname);
|
||||
if (app.te_delete_use_count > 0) {
|
||||
ImGui::TextColored(ImVec4(0.95f, 0.6f, 0.2f, 1.0f),
|
||||
"Hay %d entidades en uso de este type_ref. Quedaran huerfanas hasta que las cambies.",
|
||||
app.te_delete_use_count);
|
||||
} else {
|
||||
ImGui::TextDisabled("Ninguna entidad usa este tipo actualmente.");
|
||||
}
|
||||
ImGui::Spacing();
|
||||
if (fn_ui::button("Delete", fn_ui::ButtonVariant::Primary)) {
|
||||
if (app.te_pending_delete_e >= 0
|
||||
&& app.te_pending_delete_e < (int)app.types_draft.entities.size()) {
|
||||
app.types_draft.entities.erase(
|
||||
app.types_draft.entities.begin() + app.te_pending_delete_e);
|
||||
if (app.te_entity_idx >= (int)app.types_draft.entities.size())
|
||||
app.te_entity_idx = (int)app.types_draft.entities.size() - 1;
|
||||
} else if (app.te_pending_delete_r >= 0
|
||||
&& app.te_pending_delete_r < (int)app.types_draft.relations.size()) {
|
||||
app.types_draft.relations.erase(
|
||||
app.types_draft.relations.begin() + app.te_pending_delete_r);
|
||||
if (app.te_relation_idx >= (int)app.types_draft.relations.size())
|
||||
app.te_relation_idx = (int)app.types_draft.relations.size() - 1;
|
||||
}
|
||||
app.te_pending_delete_e = -1;
|
||||
app.te_pending_delete_r = -1;
|
||||
app.te_delete_use_count = 0;
|
||||
app.types_dirty = true;
|
||||
app.show_te_delete_modal = false;
|
||||
confirmed = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.te_pending_delete_e = -1;
|
||||
app.te_pending_delete_r = -1;
|
||||
app.te_delete_use_count = 0;
|
||||
app.show_te_delete_modal = false;
|
||||
}
|
||||
}
|
||||
fn_ui::modal_dialog_end();
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
} // namespace ge
|
||||
|
||||
@@ -137,6 +137,24 @@ struct AppState {
|
||||
std::vector<std::string> insp_tag_suggestions;
|
||||
std::vector<std::string> insp_type_options;
|
||||
|
||||
// ---- Type Editor (issue 0007) ------------------------------------------
|
||||
// Draft del editor de tipos. Se inicializa con una copia de parsed_types
|
||||
// tras cargar el grafo. Save reescribe `types.yaml` y dispara
|
||||
// apply_types_yaml + rebuild de IconAtlas.
|
||||
bool panel_type_editor = false;
|
||||
ParsedTypes types_draft;
|
||||
bool types_dirty = false;
|
||||
int te_tab_idx = 0; // 0=Entities 1=Relations
|
||||
int te_entity_idx = -1; // seleccion entity
|
||||
int te_relation_idx = -1; // seleccion relation
|
||||
bool want_types_save = false;
|
||||
bool want_types_reload = false;
|
||||
int te_pending_delete_e = -1; // entity idx pendiente de confirmar
|
||||
int te_pending_delete_r = -1;
|
||||
int te_delete_use_count = 0; // entidades afectadas
|
||||
bool show_te_delete_modal = false;
|
||||
std::string types_save_error; // mensaje a renderizar bajo Save
|
||||
|
||||
// ---- Filtros y busqueda FTS5 (issue 0009) ------------------------------
|
||||
// Modos: 0 = highlight (no-match dimmed), 1 = hide (no-match invisible).
|
||||
enum FilterMode { FM_HIGHLIGHT = 0, FM_HIDE = 1 };
|
||||
@@ -205,6 +223,18 @@ EntityRecord views_inspector_build_record(const AppState& app);
|
||||
// al cambiar de proyecto.
|
||||
void views_inspector_clear_draft(AppState& app);
|
||||
|
||||
// ---- Type Editor (issue 0007) -------------------------------------------
|
||||
|
||||
// Renderiza el panel "Types" — tabs Entities/Relations, lista a la izquierda
|
||||
// con +/-, panel de edicion a la derecha. Marca app.types_dirty al cambiar y
|
||||
// activa app.want_types_save / app.want_types_reload desde el footer.
|
||||
void views_type_editor(AppState& app);
|
||||
|
||||
// Modal de confirmacion para borrar un tipo en uso. Se abre cuando
|
||||
// app.show_te_delete_modal = true. main.cpp es responsable de poblar
|
||||
// te_delete_use_count via consulta a operations.db antes de mostrarlo.
|
||||
bool views_type_editor_delete_modal(AppState& app);
|
||||
|
||||
// ---- Filter helpers (issue 0009) -----------------------------------------
|
||||
|
||||
// True si el filtro tiene query no vacia o al menos un tag activo.
|
||||
|
||||
Reference in New Issue
Block a user