feat(projects): integrate project switcher in app shell + Inspector menu
main.cpp: - Forward decl + switch_to_project: cierra layout_store, libera grafo, aplica nuevos paths, vuelve a cargar. - apply_project_paths: deriva operations.db/types.yaml/graph_explorer.db del slug y los expone a g_app.active_project. - main: arg --project <slug>; modo legacy si --input/positional dado; modo proyecto si no — migra layout legacy, decide target via arg/last_active/'default', crea si no existe, abre BDs y carga. - render(): handler want_switch_project + monta views_new_project_modal. views.h: AppState gana active_project, want_switch_project, switch_project_target, show_new_project_modal, new_project_buf, new_project_error, project_list_cache, project_recent_cache. views.cpp: - Toolbar: boton 'Project: <slug>' con popup (New/Recent/Open/Reveal). Refresca caches al abrir el menu. - views_new_project_modal: input slug + validacion + creacion + switch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
#include "types_registry.h"
|
||||
#include "layout_store.h"
|
||||
#include "entity_ops.h"
|
||||
#include "project_manager.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -48,6 +49,12 @@ static std::string g_layout_initial; // --layout flag
|
||||
static uint64_t g_graph_hash = 0;
|
||||
static bool g_loaded = false;
|
||||
|
||||
// Project state — paths derivados del proyecto activo. En modo legacy
|
||||
// (--input/positional explicito), `g_active_project` queda vacio y los paths
|
||||
// vienen del CLI directamente.
|
||||
static std::string g_active_project;
|
||||
static std::string g_layout_db_path; // ruta de graph_explorer.db
|
||||
|
||||
// Force layout GPU context (lazy init).
|
||||
static ForceLayoutGPU* g_gpu_ctx = nullptr;
|
||||
static bool g_gpu_dirty = true;
|
||||
@@ -101,6 +108,60 @@ static void apply_static_layout(int mode) {
|
||||
}
|
||||
}
|
||||
|
||||
// Forward decl — definido mas abajo, lo necesita switch_to_project.
|
||||
static bool load_input();
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Project lifecycle
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Aplica los paths del proyecto `slug` a las globales (g_input_path,
|
||||
// g_types_path, g_layout_db_path) y actualiza g_active_project. No abre BDs
|
||||
// ni carga el grafo — eso lo hace el caller.
|
||||
static void apply_project_paths(const std::string& slug) {
|
||||
ge::ProjectPaths p = ge::project_paths(slug.c_str());
|
||||
g_active_project = slug;
|
||||
g_input_path = p.operations_db;
|
||||
g_types_path = p.types_yaml;
|
||||
g_layout_db_path = p.layout_db;
|
||||
g_app.active_project = slug;
|
||||
}
|
||||
|
||||
// Cambia al proyecto `slug`: cierra layout_store, libera grafo, abre BDs
|
||||
// nuevas, carga grafo, persiste como last_active. Devuelve true en exito.
|
||||
static bool switch_to_project(const std::string& slug) {
|
||||
if (slug.empty()) return false;
|
||||
if (!ge::project_exists(slug.c_str())) {
|
||||
std::fprintf(stderr, "[graph_explorer] project '%s' no existe\n",
|
||||
slug.c_str());
|
||||
return false;
|
||||
}
|
||||
// Cierra estado del proyecto actual
|
||||
ge::layout_store_close();
|
||||
if (g_loaded) {
|
||||
graph::graph_free(&g_graph);
|
||||
g_loaded = false;
|
||||
}
|
||||
if (g_atlas) {
|
||||
graph_icons_destroy(g_atlas);
|
||||
g_atlas = nullptr;
|
||||
}
|
||||
g_atlas_bound = false;
|
||||
g_viewport.selection.clear();
|
||||
g_viewport.hovered_node = -1;
|
||||
g_viewport.selected_node = -1;
|
||||
|
||||
// Aplica paths nuevos y abre BDs
|
||||
apply_project_paths(slug);
|
||||
if (!ge::layout_store_open(g_layout_db_path.c_str())) {
|
||||
std::fprintf(stderr, "[graph_explorer] layout_store_open failed: %s\n",
|
||||
g_layout_db_path.c_str());
|
||||
}
|
||||
bool ok = load_input();
|
||||
if (ok) ge::project_settings_touch(slug.c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool load_input() {
|
||||
g_input.kind = ge::INPUT_OPERATIONS;
|
||||
g_input.uri = g_input_path.c_str();
|
||||
@@ -424,6 +485,18 @@ static void render() {
|
||||
// Modals
|
||||
ge::views_open_modal(g_app);
|
||||
ge::views_filters_modal(g_app);
|
||||
ge::views_new_project_modal(g_app);
|
||||
|
||||
// Project switch (desde menu Project, o tras crear proyecto nuevo)
|
||||
if (g_app.want_switch_project && !g_app.switch_project_target.empty()) {
|
||||
std::string target = g_app.switch_project_target;
|
||||
g_app.want_switch_project = false;
|
||||
g_app.switch_project_target.clear();
|
||||
if (!switch_to_project(target)) {
|
||||
std::fprintf(stderr,
|
||||
"[graph_explorer] switch_to_project('%s') failed\n", target.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Si el usuario aplico nuevo layout en la toolbar
|
||||
if (g_app.apply_layout_tick > 0) {
|
||||
@@ -685,10 +758,14 @@ static void usage() {
|
||||
"Usage: graph_explorer [<operations.db>]\n"
|
||||
" graph_explorer --input operations <path>\n"
|
||||
" graph_explorer --types <types.yaml>\n"
|
||||
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n");
|
||||
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n"
|
||||
" graph_explorer --project <slug>\n");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
bool legacy_mode = false; // --input / positional dado: NO usar proyecto
|
||||
std::string project_arg; // --project <slug> (puede estar vacio)
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const char* a = argv[i];
|
||||
if (std::strcmp(a, "--input") == 0 && i + 2 < argc) {
|
||||
@@ -696,6 +773,7 @@ int main(int argc, char** argv) {
|
||||
const char* path = argv[++i];
|
||||
if (std::strcmp(kind, "operations") == 0) {
|
||||
g_input_path = path;
|
||||
legacy_mode = true;
|
||||
} else {
|
||||
std::fprintf(stderr, "[graph_explorer] unsupported input kind: %s\n", kind);
|
||||
return 1;
|
||||
@@ -704,6 +782,8 @@ int main(int argc, char** argv) {
|
||||
g_types_path = argv[++i];
|
||||
} else if (std::strcmp(a, "--layout") == 0 && i + 1 < argc) {
|
||||
g_layout_initial = argv[++i];
|
||||
} else if (std::strcmp(a, "--project") == 0 && i + 1 < argc) {
|
||||
project_arg = argv[++i];
|
||||
} else if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) {
|
||||
usage();
|
||||
return 0;
|
||||
@@ -712,25 +792,49 @@ int main(int argc, char** argv) {
|
||||
usage();
|
||||
return 1;
|
||||
} else {
|
||||
// Positional: tratado como operations.db
|
||||
if (g_input_path.empty()) g_input_path = a;
|
||||
// Positional: tratado como operations.db (legacy)
|
||||
if (g_input_path.empty()) {
|
||||
g_input_path = a;
|
||||
legacy_mode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SQLite store junto al exe.
|
||||
ge::layout_store_open("graph_explorer.db");
|
||||
|
||||
// Si no llego --input/positional, intentar operations.db en el cwd
|
||||
// (mismo criterio que graph_explorer.db: relativo al directorio de ejecucion).
|
||||
if (g_input_path.empty()) {
|
||||
if (FILE* f = std::fopen("operations.db", "rb")) {
|
||||
std::fclose(f);
|
||||
g_input_path = "operations.db";
|
||||
std::fprintf(stdout, "[graph_explorer] using default ./operations.db\n");
|
||||
if (legacy_mode) {
|
||||
// Modo legacy: paths sueltos junto al exe (compat con flujo anterior)
|
||||
ge::layout_store_open("graph_explorer.db");
|
||||
g_layout_db_path = "graph_explorer.db";
|
||||
if (!g_input_path.empty()) {
|
||||
load_input();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Modo proyecto: migra layout legacy si aplica, decide proyecto activo,
|
||||
// crea default si no existe ninguno.
|
||||
ge::projects_migrate_legacy_layout();
|
||||
|
||||
if (!g_input_path.empty()) {
|
||||
std::string target = project_arg;
|
||||
if (target.empty()) {
|
||||
ge::ProjectSettings ps;
|
||||
ge::project_settings_load(&ps);
|
||||
target = ps.last_active;
|
||||
}
|
||||
if (target.empty()) target = ge::k_default_project;
|
||||
|
||||
if (!ge::project_exists(target.c_str())) {
|
||||
std::string err;
|
||||
if (!ge::project_create(target.c_str(), &err)) {
|
||||
std::fprintf(stderr,
|
||||
"[graph_explorer] no se pudo crear el proyecto '%s': %s\n",
|
||||
target.c_str(), err.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::fprintf(stdout,
|
||||
"[graph_explorer] proyecto creado: projects/%s/\n", target.c_str());
|
||||
}
|
||||
|
||||
apply_project_paths(target);
|
||||
ge::layout_store_open(g_layout_db_path.c_str());
|
||||
ge::project_settings_touch(target.c_str());
|
||||
load_input();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "views.h"
|
||||
#include "entity_ops.h"
|
||||
#include "project_manager.h"
|
||||
|
||||
#include "viz/graph_types.h"
|
||||
#include "viz/graph_viewport.h"
|
||||
@@ -91,6 +92,74 @@ void views_apply_visibility(AppState& app) {
|
||||
void views_toolbar(AppState& app) {
|
||||
using namespace fn_ui;
|
||||
toolbar_begin();
|
||||
// Project switcher — etiqueta = proyecto activo. Click abre popup con
|
||||
// New / Open / Recent / Reveal. Si no hay proyecto activo (modo
|
||||
// legacy con --input directo), muestra "(no project)".
|
||||
{
|
||||
char btn_label[128];
|
||||
const char* p = app.active_project.empty()
|
||||
? "(no project)" : app.active_project.c_str();
|
||||
std::snprintf(btn_label, sizeof(btn_label), TI_FOLDER " Project: %s", p);
|
||||
if (button(btn_label, ButtonVariant::Secondary)) {
|
||||
// Refresca caches al abrir
|
||||
project_list(&app.project_list_cache);
|
||||
ProjectSettings ps;
|
||||
project_settings_load(&ps);
|
||||
app.project_recent_cache = ps.recent;
|
||||
ImGui::OpenPopup("##project_menu");
|
||||
}
|
||||
if (ImGui::BeginPopup("##project_menu")) {
|
||||
if (ImGui::MenuItem(TI_PLUS " New project...")) {
|
||||
app.show_new_project_modal = true;
|
||||
app.new_project_buf[0] = 0;
|
||||
app.new_project_error.clear();
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// Recent submenu
|
||||
if (!app.project_recent_cache.empty()) {
|
||||
if (ImGui::BeginMenu("Recent")) {
|
||||
for (const auto& slug : app.project_recent_cache) {
|
||||
bool is_active = (slug == app.active_project);
|
||||
if (ImGui::MenuItem(slug.c_str(), nullptr, is_active)
|
||||
&& !is_active) {
|
||||
app.want_switch_project = true;
|
||||
app.switch_project_target = slug;
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
// Open submenu — todos los detectados
|
||||
if (ImGui::BeginMenu("Open")) {
|
||||
if (app.project_list_cache.empty()) {
|
||||
ImGui::TextDisabled("(no projects yet)");
|
||||
} else {
|
||||
for (const auto& slug : app.project_list_cache) {
|
||||
bool is_active = (slug == app.active_project);
|
||||
if (ImGui::MenuItem(slug.c_str(), nullptr, is_active)
|
||||
&& !is_active) {
|
||||
app.want_switch_project = true;
|
||||
app.switch_project_target = slug;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
bool can_reveal = !app.active_project.empty();
|
||||
if (ImGui::MenuItem(TI_FOLDER_OPEN " Reveal in explorer",
|
||||
nullptr, false, can_reveal)) {
|
||||
project_reveal_in_explorer(app.active_project.c_str());
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
toolbar_separator();
|
||||
|
||||
if (button(TI_FOLDER " Open file...", ButtonVariant::Secondary)) {
|
||||
app.show_open_modal = true;
|
||||
}
|
||||
@@ -490,6 +559,58 @@ bool views_filters_modal(AppState& app) {
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Modal: New project
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
bool views_new_project_modal(AppState& app) {
|
||||
if (!app.show_new_project_modal) return false;
|
||||
bool created = false;
|
||||
if (fn_ui::modal_dialog_begin("New project", &app.show_new_project_modal,
|
||||
ImVec2(480, 0))) {
|
||||
ImGui::TextWrapped(
|
||||
"Crea una subcarpeta en projects/ con su propio operations.db,"
|
||||
" types.yaml y graph_explorer.db.");
|
||||
ImGui::Spacing();
|
||||
fn_ui::text_input("Slug", app.new_project_buf, sizeof(app.new_project_buf),
|
||||
"caso_aurgi");
|
||||
ImGui::TextDisabled("a-z, 0-9, '_' y '-' (max 64)");
|
||||
|
||||
if (!app.new_project_error.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.5f, 0.4f, 1.0f));
|
||||
ImGui::TextWrapped("%s", app.new_project_error.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
if (fn_ui::button("Create", fn_ui::ButtonVariant::Primary)) {
|
||||
std::string err;
|
||||
if (!project_validate_slug(app.new_project_buf, &err)) {
|
||||
app.new_project_error = err;
|
||||
} else if (project_exists(app.new_project_buf)) {
|
||||
app.new_project_error = "ya existe un proyecto con ese slug";
|
||||
} else if (!project_create(app.new_project_buf, &err)) {
|
||||
app.new_project_error = err;
|
||||
} else {
|
||||
// Switch al recien creado
|
||||
app.want_switch_project = true;
|
||||
app.switch_project_target = app.new_project_buf;
|
||||
app.show_new_project_modal = false;
|
||||
app.new_project_error.clear();
|
||||
created = true;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (fn_ui::button("Cancel", fn_ui::ButtonVariant::Subtle)) {
|
||||
app.show_new_project_modal = false;
|
||||
app.new_project_error.clear();
|
||||
}
|
||||
}
|
||||
fn_ui::modal_dialog_end();
|
||||
return created;
|
||||
}
|
||||
|
||||
bool views_open_modal(AppState& app) {
|
||||
if (!app.show_open_modal) return false;
|
||||
bool opened = false;
|
||||
|
||||
@@ -52,6 +52,16 @@ struct AppState {
|
||||
bool want_open_file = false; // marcado al confirmar el modal Open
|
||||
char open_buf[512] = {};
|
||||
|
||||
// Project system (issue 0006)
|
||||
std::string active_project; // slug del proyecto activo
|
||||
bool want_switch_project = false;
|
||||
std::string switch_project_target; // slug objetivo del switch
|
||||
bool show_new_project_modal = false;
|
||||
char new_project_buf[80] = {};
|
||||
std::string new_project_error; // mensaje a mostrar en el modal
|
||||
std::vector<std::string> project_list_cache; // refrescado al abrir el menu Project
|
||||
std::vector<std::string> project_recent_cache;
|
||||
|
||||
// Labels overlay
|
||||
bool labels_enabled = true;
|
||||
|
||||
@@ -108,6 +118,10 @@ bool views_filters_modal(AppState& app);
|
||||
// Modal Open file — text input + boton Open.
|
||||
bool views_open_modal(AppState& app);
|
||||
|
||||
// Modal "New project..." — slug input con validacion. Devuelve true si el
|
||||
// usuario confirma con un slug valido.
|
||||
bool views_new_project_modal(AppState& app);
|
||||
|
||||
// Refresca los flags `flags` de cada nodo/arista segun el array
|
||||
// `type_visible[]` / `rel_type_visible[]`. Lineal en N+M.
|
||||
void views_apply_visibility(AppState& app);
|
||||
|
||||
Reference in New Issue
Block a user