merge: iter2 — fixes (docking/hover/conflict) + reset layout + spread + markdown notes
This commit is contained in:
@@ -286,6 +286,49 @@ bool relation_insert(const char* db_path, const char* from_id, const char* to_id
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Notes (markdown)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool entity_get_notes(const char* db_path, const char* id, std::string* out) {
|
||||||
|
if (!db_path || !id || !out) return false;
|
||||||
|
out->clear();
|
||||||
|
sqlite3* db = nullptr;
|
||||||
|
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, nullptr) != SQLITE_OK) {
|
||||||
|
if (db) sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sqlite3_stmt* st = nullptr;
|
||||||
|
if (sqlite3_prepare_v2(db, "SELECT notes FROM entities WHERE id = ?", -1, &st, nullptr) != SQLITE_OK) {
|
||||||
|
sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sqlite3_bind_text(st, 1, id, -1, SQLITE_TRANSIENT);
|
||||||
|
bool ok = false;
|
||||||
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
const unsigned char* p = sqlite3_column_text(st, 0);
|
||||||
|
if (p) *out = (const char*)p;
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
sqlite3_finalize(st);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool entity_set_notes(const char* db_path, const char* id, const char* notes) {
|
||||||
|
if (!db_path || !id) return false;
|
||||||
|
sqlite3* db = nullptr;
|
||||||
|
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK) {
|
||||||
|
if (db) sqlite3_close(db);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string ts = now_iso();
|
||||||
|
const char* p[3] = { notes ? notes : "", ts.c_str(), id };
|
||||||
|
bool ok = exec_one(db, "UPDATE entities SET notes = ?, updated_at = ? WHERE id = ?", p, 3);
|
||||||
|
sqlite3_close(db);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Index user_data -> sql id
|
// Index user_data -> sql id
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ bool relation_insert(const char* db_path,
|
|||||||
const char* from_id, const char* to_id,
|
const char* from_id, const char* to_id,
|
||||||
const char* name);
|
const char* name);
|
||||||
|
|
||||||
|
// Lee la columna `notes` (markdown) de una entidad. out se reasigna; vacio si
|
||||||
|
// no existe la entidad.
|
||||||
|
bool entity_get_notes(const char* db_path, const char* id, std::string* out);
|
||||||
|
|
||||||
|
// Sobrescribe `notes` con el contenido proporcionado. Toca `updated_at`.
|
||||||
|
bool entity_set_notes(const char* db_path, const char* id, const char* notes);
|
||||||
|
|
||||||
// Mapa user_data (FNV1a hash) -> sql id. Se reconstruye despues de cada
|
// Mapa user_data (FNV1a hash) -> sql id. Se reconstruye despues de cada
|
||||||
// carga del grafo (graph_sources usa FNV1a sobre id como user_data).
|
// carga del grafo (graph_sources usa FNV1a sobre id como user_data).
|
||||||
struct EntityIndex {
|
struct EntityIndex {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "core/panel_menu.h"
|
#include "core/panel_menu.h"
|
||||||
#include "core/button.h"
|
#include "core/button.h"
|
||||||
#include "core/tokens.h"
|
#include "core/tokens.h"
|
||||||
|
#include "core/icons_tabler.h"
|
||||||
|
|
||||||
#include "viz/graph_types.h"
|
#include "viz/graph_types.h"
|
||||||
#include "viz/graph_viewport.h"
|
#include "viz/graph_viewport.h"
|
||||||
@@ -28,7 +29,9 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Estado global de la app
|
// Estado global de la app
|
||||||
@@ -246,6 +249,13 @@ static void update_fps() {
|
|||||||
// Context menu callback (right-click sobre nodo)
|
// Context menu callback (right-click sobre nodo)
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Doble click sobre nodo: solicita abrir el panel Note. main.cpp procesa
|
||||||
|
// despues (necesita acceso al EntityIndex para resolver el sql id).
|
||||||
|
static void on_double_click_cb(int node_idx, void* /*user*/) {
|
||||||
|
g_app.want_open_note = true;
|
||||||
|
g_app.open_note_target = node_idx;
|
||||||
|
}
|
||||||
|
|
||||||
static void on_context_menu_cb(int node_idx, ImVec2 /*screen_pos*/, void* /*user*/) {
|
static void on_context_menu_cb(int node_idx, ImVec2 /*screen_pos*/, void* /*user*/) {
|
||||||
g_app.ctx_node = node_idx;
|
g_app.ctx_node = node_idx;
|
||||||
g_app.ctx_open_request = true;
|
g_app.ctx_open_request = true;
|
||||||
@@ -287,23 +297,28 @@ static void render_context_menu() {
|
|||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
if (ImGui::BeginMenu("Change type")) {
|
if (ImGui::BeginMenu("Change type")) {
|
||||||
// Tipos del grafo actual
|
// Construye un set ordenado y deduplicado: tipos del grafo + defaults.
|
||||||
|
// Asi evitamos colisiones de ID en ImGui ("person" en grafo y default).
|
||||||
|
std::vector<const char*> all;
|
||||||
|
all.reserve(g_graph.type_count + k_default_types_n);
|
||||||
for (int i = 0; i < g_graph.type_count; ++i) {
|
for (int i = 0; i < g_graph.type_count; ++i) {
|
||||||
const char* name = g_graph.types[i].name;
|
if (g_graph.types[i].name && *g_graph.types[i].name) {
|
||||||
if (!name) continue;
|
all.push_back(g_graph.types[i].name);
|
||||||
if (ImGui::MenuItem(name)) {
|
|
||||||
std::snprintf(g_app.ctx_new_type, sizeof(g_app.ctx_new_type), "%s", name);
|
|
||||||
g_app.want_change_type = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
|
||||||
// Defaults extra (por si no estan presentes en el grafo cargado)
|
|
||||||
for (int i = 0; i < k_default_types_n; ++i) {
|
for (int i = 0; i < k_default_types_n; ++i) {
|
||||||
if (ImGui::MenuItem(k_default_types[i])) {
|
const char* d = k_default_types[i];
|
||||||
std::snprintf(g_app.ctx_new_type, sizeof(g_app.ctx_new_type), "%s",
|
bool dup = false;
|
||||||
k_default_types[i]);
|
for (const char* x : all) { if (std::strcmp(x, d) == 0) { dup = true; break; } }
|
||||||
|
if (!dup) all.push_back(d);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < all.size(); ++i) {
|
||||||
|
ImGui::PushID((int)i);
|
||||||
|
if (ImGui::MenuItem(all[i])) {
|
||||||
|
std::snprintf(g_app.ctx_new_type, sizeof(g_app.ctx_new_type), "%s", all[i]);
|
||||||
g_app.want_change_type = true;
|
g_app.want_change_type = true;
|
||||||
}
|
}
|
||||||
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
@@ -343,6 +358,7 @@ static fn_ui::PanelToggle g_panels[] = {
|
|||||||
{"Legend", nullptr, &g_app.panel_legend},
|
{"Legend", nullptr, &g_app.panel_legend},
|
||||||
{"Inspector", nullptr, &g_app.panel_inspector},
|
{"Inspector", nullptr, &g_app.panel_inspector},
|
||||||
{"Stats", nullptr, &g_app.panel_stats},
|
{"Stats", nullptr, &g_app.panel_stats},
|
||||||
|
{"Note", nullptr, &g_app.panel_note},
|
||||||
};
|
};
|
||||||
|
|
||||||
static void render() {
|
static void render() {
|
||||||
@@ -372,13 +388,13 @@ static void render() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dockspace host: ocupa el area de trabajo bajo la menubar y permite
|
// Dockspace host: ocupa el area BAJO la toolbar (44 px) para que las
|
||||||
// que cualquier ventana (Viewport, Legend, Inspector, Stats, Table) se
|
// ventanas dockeadas no queden detras de la barra superior.
|
||||||
// arrastre a un lateral o pestañas dentro de la app.
|
|
||||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||||
|
const float k_toolbar_h = 44.0f;
|
||||||
{
|
{
|
||||||
ImGui::SetNextWindowPos (vp->WorkPos);
|
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x, vp->WorkPos.y + k_toolbar_h));
|
||||||
ImGui::SetNextWindowSize(vp->WorkSize);
|
ImGui::SetNextWindowSize(ImVec2(vp->WorkSize.x, vp->WorkSize.y - k_toolbar_h));
|
||||||
ImGui::SetNextWindowViewport(vp->ID);
|
ImGui::SetNextWindowViewport(vp->ID);
|
||||||
ImGuiWindowFlags hostFlags =
|
ImGuiWindowFlags hostFlags =
|
||||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||||
@@ -450,16 +466,38 @@ static void render() {
|
|||||||
// ---- Mutaciones (add/delete/duplicate/change_type) ----
|
// ---- Mutaciones (add/delete/duplicate/change_type) ----
|
||||||
auto reload_after_mutation = [&]() {
|
auto reload_after_mutation = [&]() {
|
||||||
graph::GraphLoadStats stats{};
|
graph::GraphLoadStats stats{};
|
||||||
if (ge::reload_graph(g_input, &g_graph, &stats)) {
|
if (!ge::reload_graph(g_input, &g_graph, &stats)) return;
|
||||||
ge::entity_index_build(g_input.uri, &g_idx);
|
ge::entity_index_build(g_input.uri, &g_idx);
|
||||||
ge::views_reset_visibility(g_app);
|
ge::views_reset_visibility(g_app);
|
||||||
ge::views_apply_visibility(g_app);
|
ge::views_apply_visibility(g_app);
|
||||||
g_graph.update_bounds();
|
|
||||||
int restored = ge::layout_store_load(g_graph_hash, g_graph);
|
// Restablece posiciones guardadas. Los nodos nuevos no tienen
|
||||||
if (restored > 0) g_graph.update_bounds();
|
// posicion en el layout_store y caen en (0,0).
|
||||||
g_atlas_bound = false;
|
int restored = ge::layout_store_load(g_graph_hash, g_graph);
|
||||||
g_gpu_dirty = true;
|
(void)restored;
|
||||||
|
|
||||||
|
// Centro del area visible en world coords (para que los nuevos nodos
|
||||||
|
// aparezcan donde el usuario esta mirando, no en el origen).
|
||||||
|
float cx = -g_viewport.cam_x;
|
||||||
|
float cy = -g_viewport.cam_y;
|
||||||
|
float spread_r = 80.0f / (g_viewport.zoom > 0.01f ? g_viewport.zoom : 0.01f);
|
||||||
|
|
||||||
|
// Reparte los nodos sin posicion en un anillo poisson alrededor del
|
||||||
|
// centro visible. Determinista por user_data para que el mismo nodo
|
||||||
|
// caiga siempre en el mismo sitio entre reloads.
|
||||||
|
for (int i = 0; i < g_graph.node_count; ++i) {
|
||||||
|
GraphNode& n = g_graph.nodes[i];
|
||||||
|
if (n.x != 0.0f || n.y != 0.0f) continue;
|
||||||
|
uint64_t h = n.user_data ? n.user_data : (uint64_t)i * 2654435761ull;
|
||||||
|
float a = (float)((h >> 0) & 0xFFFF) / 65535.0f * 6.2831853f;
|
||||||
|
float r = spread_r * (0.4f + (float)((h >> 16) & 0xFFFF) / 65535.0f * 0.6f);
|
||||||
|
n.x = cx + std::cos(a) * r;
|
||||||
|
n.y = cy + std::sin(a) * r;
|
||||||
|
n.vx = n.vy = 0.0f;
|
||||||
}
|
}
|
||||||
|
g_graph.update_bounds();
|
||||||
|
g_atlas_bound = false;
|
||||||
|
g_gpu_dirty = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (g_app.want_add_node && g_app.add_buf[0]) {
|
if (g_app.want_add_node && g_app.add_buf[0]) {
|
||||||
@@ -513,6 +551,61 @@ static void render() {
|
|||||||
g_app.want_change_type = false;
|
g_app.want_change_type = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset layout: limpia NF_PINNED en todos los nodos. El layout activo se
|
||||||
|
// reaplica via apply_layout_tick (la toolbar ya lo incrementa).
|
||||||
|
if (g_app.want_unpin_all) {
|
||||||
|
for (int i = 0; i < g_graph.node_count; ++i) {
|
||||||
|
g_graph.nodes[i].flags &= ~NF_PINNED;
|
||||||
|
g_graph.nodes[i].vx = 0.0f;
|
||||||
|
g_graph.nodes[i].vy = 0.0f;
|
||||||
|
}
|
||||||
|
g_viewport.layout_running = true;
|
||||||
|
g_app.want_unpin_all = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note editor — abrir / guardar.
|
||||||
|
if (g_app.want_open_note && g_app.open_note_target >= 0
|
||||||
|
&& g_app.open_note_target < g_graph.node_count) {
|
||||||
|
int n = g_app.open_note_target;
|
||||||
|
const char* sql_id = ge::entity_index_lookup(g_idx, g_graph.nodes[n].user_data);
|
||||||
|
if (sql_id) {
|
||||||
|
std::string md;
|
||||||
|
ge::entity_get_notes(g_app.input_db_path.c_str(), sql_id, &md);
|
||||||
|
g_app.note_node = n;
|
||||||
|
g_app.note_entity_id = sql_id;
|
||||||
|
const char* lbl = graph::graph_label(&g_graph, g_graph.nodes[n].label_idx);
|
||||||
|
g_app.note_entity_label = lbl ? lbl : "";
|
||||||
|
uint16_t tid = g_graph.nodes[n].type_id;
|
||||||
|
g_app.note_entity_type = (tid < (uint16_t)g_graph.type_count
|
||||||
|
&& g_graph.types[tid].name)
|
||||||
|
? g_graph.types[tid].name : "";
|
||||||
|
// Asegura buffer >= max(64KB, contenido + holgura).
|
||||||
|
size_t need = md.size() + 4096;
|
||||||
|
if (need < 65536) need = 65536;
|
||||||
|
g_app.note_buf.assign(need, 0);
|
||||||
|
std::memcpy(g_app.note_buf.data(), md.data(), md.size());
|
||||||
|
g_app.note_dirty = false;
|
||||||
|
g_app.panel_note = true;
|
||||||
|
ImGui::SetWindowFocus(TI_FILE_TEXT " Note");
|
||||||
|
}
|
||||||
|
g_app.want_open_note = false;
|
||||||
|
g_app.open_note_target = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_app.want_save_note && !g_app.note_entity_id.empty()) {
|
||||||
|
if (ge::entity_set_notes(g_app.input_db_path.c_str(),
|
||||||
|
g_app.note_entity_id.c_str(),
|
||||||
|
g_app.note_buf.data())) {
|
||||||
|
g_app.note_dirty = false;
|
||||||
|
std::fprintf(stdout, "[graph_explorer] saved note for %s (%zu bytes)\n",
|
||||||
|
g_app.note_entity_id.c_str(),
|
||||||
|
std::strlen(g_app.note_buf.data()));
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr, "[graph_explorer] save note failed\n");
|
||||||
|
}
|
||||||
|
g_app.want_save_note = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Posiciones iniciales razonables; el usuario puede moverlas y se
|
// Posiciones iniciales razonables; el usuario puede moverlas y se
|
||||||
// persiste via imgui.ini.
|
// persiste via imgui.ini.
|
||||||
const float top = vp->WorkPos.y + 44.0f;
|
const float top = vp->WorkPos.y + 44.0f;
|
||||||
@@ -531,6 +624,7 @@ static void render() {
|
|||||||
|
|
||||||
GraphViewportCallbacks vp_cb{};
|
GraphViewportCallbacks vp_cb{};
|
||||||
vp_cb.on_context_menu = &on_context_menu_cb;
|
vp_cb.on_context_menu = &on_context_menu_cb;
|
||||||
|
vp_cb.on_double_click = &on_double_click_cb;
|
||||||
graph_viewport("##gv", g_graph, g_viewport, ImVec2(0, 0), vp_cb);
|
graph_viewport("##gv", g_graph, g_viewport, ImVec2(0, 0), vp_cb);
|
||||||
render_context_menu();
|
render_context_menu();
|
||||||
|
|
||||||
@@ -572,6 +666,13 @@ static void render() {
|
|||||||
ImGui::SetNextWindowSize(ImVec2(rw, H - sh), ImGuiCond_FirstUseEver);
|
ImGui::SetNextWindowSize(ImVec2(rw, H - sh), ImGuiCond_FirstUseEver);
|
||||||
ge::views_stats(g_app);
|
ge::views_stats(g_app);
|
||||||
|
|
||||||
|
// Note editor — al abrirse por primera vez se posiciona como ventana
|
||||||
|
// centrada. El usuario la puede dockear donde prefiera.
|
||||||
|
ImGui::SetNextWindowPos (ImVec2(vp->WorkPos.x + W * 0.25f, top + 40.0f),
|
||||||
|
ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(700.0f, 480.0f), ImGuiCond_FirstUseEver);
|
||||||
|
ge::views_note(g_app);
|
||||||
|
|
||||||
g_first_render = false;
|
g_first_render = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ void views_toolbar(AppState& app) {
|
|||||||
if (button(TI_REFRESH " Reload", ButtonVariant::Subtle)) {
|
if (button(TI_REFRESH " Reload", ButtonVariant::Subtle)) {
|
||||||
app.want_reload = true;
|
app.want_reload = true;
|
||||||
}
|
}
|
||||||
|
if (button(TI_LAYOUT_GRID " Reset layout", ButtonVariant::Subtle)) {
|
||||||
|
app.want_unpin_all = true;
|
||||||
|
++app.apply_layout_tick;
|
||||||
|
}
|
||||||
toolbar_separator();
|
toolbar_separator();
|
||||||
|
|
||||||
ImGui::Checkbox("GPU layout", &app.use_gpu);
|
ImGui::Checkbox("GPU layout", &app.use_gpu);
|
||||||
@@ -338,6 +342,78 @@ void views_stats(AppState& app) {
|
|||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Note editor (markdown)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void views_note(AppState& app) {
|
||||||
|
if (!app.panel_note) return;
|
||||||
|
if (!ImGui::Begin(TI_FILE_TEXT " Note", &app.panel_note,
|
||||||
|
ImGuiWindowFlags_MenuBar)) {
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginMenuBar()) {
|
||||||
|
if (ImGui::MenuItem(TI_DEVICE_FLOPPY " Save", "Ctrl+S",
|
||||||
|
/*selected=*/false, app.note_dirty)) {
|
||||||
|
app.want_save_note = true;
|
||||||
|
}
|
||||||
|
if (app.note_dirty) {
|
||||||
|
ImGui::TextDisabled("(modified)");
|
||||||
|
}
|
||||||
|
ImGui::EndMenuBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.note_node < 0) {
|
||||||
|
ImGui::TextDisabled("Doble click sobre un nodo para abrir su nota.");
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||||
|
ImGui::TextUnformatted("entity:");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextUnformatted(app.note_entity_label.empty()
|
||||||
|
? "(unnamed)" : app.note_entity_label.c_str());
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||||
|
ImGui::TextUnformatted("type:");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextUnformatted(app.note_entity_type.empty()
|
||||||
|
? "(no-type)" : app.note_entity_type.c_str());
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted);
|
||||||
|
ImGui::TextUnformatted("id:");
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextUnformatted(app.note_entity_id.c_str());
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (app.note_buf.empty()) app.note_buf.resize(1024, 0);
|
||||||
|
|
||||||
|
// Crece el buffer si esta cerca del limite.
|
||||||
|
if (std::strlen(app.note_buf.data()) + 64 >= app.note_buf.size()) {
|
||||||
|
app.note_buf.resize(app.note_buf.size() * 2, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 size = ImGui::GetContentRegionAvail();
|
||||||
|
if (ImGui::InputTextMultiline("##note_md", app.note_buf.data(),
|
||||||
|
app.note_buf.size(), size,
|
||||||
|
ImGuiInputTextFlags_AllowTabInput)) {
|
||||||
|
app.note_dirty = true;
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered() &&
|
||||||
|
ImGui::IsKeyChordPressed(ImGuiMod_Ctrl | ImGuiKey_S)) {
|
||||||
|
app.want_save_note = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Modals
|
// Modals
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
struct GraphData;
|
struct GraphData;
|
||||||
struct GraphViewportState;
|
struct GraphViewportState;
|
||||||
@@ -14,9 +15,11 @@ struct AppState {
|
|||||||
GraphData* graph = nullptr;
|
GraphData* graph = nullptr;
|
||||||
GraphViewportState* viewport = nullptr;
|
GraphViewportState* viewport = nullptr;
|
||||||
|
|
||||||
// Layout activo
|
// Layout activo — default grid (1) para que los grafos cargados de
|
||||||
int layout_mode = 0; // 0=force, 1=grid, 2=circular, 3=radial, 4=hierarchical, 5=fixed
|
// operations.db se distribuyan ordenadamente al abrir.
|
||||||
|
int layout_mode = 1; // 0=force, 1=grid, 2=circular, 3=radial, 4=hierarchical, 5=fixed
|
||||||
int apply_layout_tick = 0; // se incrementa cuando hay que reaplicar layout
|
int apply_layout_tick = 0; // se incrementa cuando hay que reaplicar layout
|
||||||
|
bool want_unpin_all = false; // Reset layout: limpia NF_PINNED y reaplica
|
||||||
|
|
||||||
// Force layout — config + GPU toggle
|
// Force layout — config + GPU toggle
|
||||||
float repulsion = 1500.0f;
|
float repulsion = 1500.0f;
|
||||||
@@ -38,6 +41,7 @@ struct AppState {
|
|||||||
bool panel_inspector = true;
|
bool panel_inspector = true;
|
||||||
bool panel_stats = true;
|
bool panel_stats = true;
|
||||||
bool panel_viewport = true;
|
bool panel_viewport = true;
|
||||||
|
bool panel_note = false;
|
||||||
bool show_filters_modal = false;
|
bool show_filters_modal = false;
|
||||||
bool show_open_modal = false;
|
bool show_open_modal = false;
|
||||||
|
|
||||||
@@ -68,6 +72,17 @@ struct AppState {
|
|||||||
|
|
||||||
// Context menu state — popup global identificado por nombre.
|
// Context menu state — popup global identificado por nombre.
|
||||||
bool ctx_open_request = false; // se setea en on_context_menu
|
bool ctx_open_request = false; // se setea en on_context_menu
|
||||||
|
|
||||||
|
// Note editor (panel "Note" abierto con doble click sobre nodo).
|
||||||
|
int note_node = -1; // node_idx siendo editado
|
||||||
|
std::string note_entity_id; // sql id resuelto
|
||||||
|
std::string note_entity_label; // display
|
||||||
|
std::string note_entity_type;
|
||||||
|
std::vector<char> note_buf; // editable, NUL-terminated
|
||||||
|
bool note_dirty = false;
|
||||||
|
bool want_save_note = false;
|
||||||
|
bool want_open_note = false; // doble click → cargar y abrir
|
||||||
|
int open_note_target = -1; // node_idx a abrir
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toolbar superior (Open file, Layout selector, Filters..., Fit, Save layout).
|
// Toolbar superior (Open file, Layout selector, Filters..., Fit, Save layout).
|
||||||
@@ -82,6 +97,10 @@ void views_inspector(AppState& app);
|
|||||||
// Stats line — counts + fps + energy + selection.
|
// Stats line — counts + fps + energy + selection.
|
||||||
void views_stats(AppState& app);
|
void views_stats(AppState& app);
|
||||||
|
|
||||||
|
// Note editor — abre con doble click sobre un nodo. Edita la columna `notes`
|
||||||
|
// (markdown) de la entidad y guarda con un boton.
|
||||||
|
void views_note(AppState& app);
|
||||||
|
|
||||||
// Modal Filters — toggles por tipo agrupados en columnas. Devuelve true si
|
// Modal Filters — toggles por tipo agrupados en columnas. Devuelve true si
|
||||||
// el usuario togglo algo.
|
// el usuario togglo algo.
|
||||||
bool views_filters_modal(AppState& app);
|
bool views_filters_modal(AppState& app);
|
||||||
|
|||||||
Reference in New Issue
Block a user