diff --git a/issues/0007-type-editor-panel.md b/issues/completed/0007-type-editor-panel.md similarity index 97% rename from issues/0007-type-editor-panel.md rename to issues/completed/0007-type-editor-panel.md index 8e70ec9..a7099c3 100644 --- a/issues/0007-type-editor-panel.md +++ b/issues/completed/0007-type-editor-panel.md @@ -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] --- diff --git a/main.cpp b/main.cpp index a1ede26..49edc21 100644 --- a/main.cpp +++ b/main.cpp @@ -26,6 +26,8 @@ #include "entity_ops.h" #include "project_manager.h" +#include "../../../../cpp/vendor/sqlite3/sqlite3.h" + #include #include #include @@ -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 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 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; } diff --git a/types_registry.cpp b/types_registry.cpp index ac24545..7551b28 100644 --- a/types_registry.cpp +++ b/types_registry.cpp @@ -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"; diff --git a/views.cpp b/views.cpp index ec01558..de41ec8 100644 --- a/views.cpp +++ b/views.cpp @@ -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 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 diff --git a/views.h b/views.h index 5c101d2..825f575 100644 --- a/views.h +++ b/views.h @@ -137,6 +137,24 @@ struct AppState { std::vector insp_tag_suggestions; std::vector 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.