feat(types): Type Editor panel — CRUD de tipos en vivo (issue 0007)

- views_type_editor: panel "Types" con tabs Entities/Relations.
  Entities: name, color picker, shape combo, icon (ti-* + cp preview),
  principal_field combo, tabla de Fields (string/int/float/bool/date/url/enum)
  con required y enum values CSV; up/down/X por fila.
  Relations: name, color, style.
  Footer Save / Reload from disk + indicador dirty + error inline.
- views_type_editor_delete_modal: confirm con conteo de entidades en uso.
- types_registry: shape_name() + shape: emit en types_save_yaml para
  round-trip estable de la cosmetica editada en UI.
- main.cpp: panel "Types" en g_panels; init types_draft tras load_input;
  want_types_save -> save + apply_types_yaml + rebuild atlas + bind +
  refresh inspector caches; want_types_reload simetrico; conteo de
  uso desde operations.db cuando se abre el modal de delete.
This commit is contained in:
2026-05-01 00:42:30 +02:00
parent 69f1afcf9e
commit f80348d604
5 changed files with 549 additions and 1 deletions
+409
View File
@@ -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