Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
TabManager.cpp
Go to the documentation of this file.
1/**
2 * @file TabManager.cpp
3 * @brief Implementation of the central multi-graph tab manager.
4 * @author Olympe Engine
5 * @date 2026-03-11
6 *
7 * @details C++14 compliant.
8 */
9
10#include "TabManager.h"
13#include "NodeGraphPanel.h"
17#include "../DataManager.h"
18
19#include "../third_party/imgui/imgui.h"
20#include "../third_party/nlohmann/json.hpp"
21#include "../system/system_utils.h"
22
23#include <fstream>
24#include <sstream>
25#include <algorithm>
26#include <cstring>
27
28namespace Olympe {
29
30// ============================================================================
31// Singleton
32// ============================================================================
33
35{
36 static TabManager instance;
37 return instance;
38}
39
41 : m_nextTabNum(1)
42 , m_nextTabIDNum(1)
43 , m_showSaveAsDialog(false)
44{
45 std::memset(m_saveAsBuffer, 0, sizeof(m_saveAsBuffer));
46}
47
49{
50 for (size_t i = 0; i < m_tabs.size(); ++i)
51 {
52 delete m_tabs[i].renderer;
53 m_tabs[i].renderer = nullptr;
54 }
55 m_tabs.clear();
56}
57
58// ============================================================================
59// ID / Name helpers
60// ============================================================================
61
63{
64 std::ostringstream oss;
65 oss << "tab_" << m_nextTabIDNum++;
66 return oss.str();
67}
68
69std::string TabManager::DisplayNameFromPath(const std::string& filePath)
70{
71 if (filePath.empty())
72 return "";
73
74 // Find last separator
75 size_t pos = filePath.find_last_of("/\\");
76 if (pos == std::string::npos)
77 return filePath;
78 return filePath.substr(pos + 1);
79}
80
81// ============================================================================
82// Graph type detection
83// ============================================================================
84
85std::string TabManager::DetectGraphType(const std::string& filePath)
86{
87 std::ifstream ifs(filePath.c_str());
88 if (!ifs.good())
89 return "Unknown";
90
92 try
93 {
94 ifs >> root;
95 }
96 catch (...)
97 {
98 return "Unknown";
99 }
100
101 if (!root.is_object())
102 return "Unknown";
103
104 // Explicit type fields
105 if (root.contains("graphType") && root["graphType"].is_string())
106 {
107 std::string gt = root["graphType"].get<std::string>();
108 if (gt == "VisualScript" || gt == "BehaviorTree" || gt == "AnimGraph")
109 return gt;
110 }
111 if (root.contains("blueprintType") && root["blueprintType"].is_string())
112 {
113 std::string bt = root["blueprintType"].get<std::string>();
114 if (bt == "BehaviorTree")
115 return "BehaviorTree";
116 if (bt == "EntityPrefab")
117 return "EntityPrefab";
118 }
119
120 // Structural heuristics
121 int schemaVersion = 0;
122 if (root.contains("schema_version") && root["schema_version"].is_number())
123 schemaVersion = root["schema_version"].get<int>();
124
125 if (schemaVersion == 4)
126 {
127 if (root.contains("nodes") && root.contains("execConnections"))
128 return "VisualScript";
129 }
130
131 if (root.contains("rootNodeId") && root.contains("nodes"))
132 return "BehaviorTree";
133
134 if (root.contains("states") && root.contains("transitions"))
135 return "AnimGraph";
136
137 // Legacy BT v2 format
138 if (root.contains("blueprintType"))
139 return "BehaviorTree";
140
141 return "Unknown";
142}
143
144// ============================================================================
145// Tab creation
146// ============================================================================
147
148std::string TabManager::CreateNewTab(const std::string& graphType)
149{
150 std::ostringstream nameSS;
151 nameSS << "Untitled-" << m_nextTabNum++;
152
154 tab.tabID = NextTabID();
155 tab.displayName = nameSS.str();
156 tab.filePath = "";
157 tab.graphType = graphType;
158 tab.isDirty = false;
159 tab.isActive = false;
160
161 if (graphType == "VisualScript")
162 {
164 tab.renderer = r;
165 }
166 else if (graphType == "EntityPrefab")
167 {
170 static bool s_epCanvasInit = false;
171 if (!s_epCanvasInit)
172 {
174 s_epCanvasInit = true;
175 }
176
178 tab.renderer = r;
179 }
180 else if (graphType == "BehaviorTree")
181 {
183 static bool s_btPanelInit = false;
184 if (!s_btPanelInit)
185 {
187 s_btPanelInit = true;
188 }
189
191 // Create new empty graph immediately so canvas appears on tab creation
192 r->CreateNew(nameSS.str());
193 tab.renderer = r;
194 }
195 else
196 {
197 //SYSTEM_LOG << "[TabManager] CreateNewTab: unsupported type '" << graphType << "'\n";
198 return "";
199 }
200
201 m_tabs.push_back(tab);
202 SetActiveTab(tab.tabID);
203 //SYSTEM_LOG << "[TabManager] Created new tab: " << tab.displayName << " (" << graphType << ")\n";
204 return tab.tabID;
205}
206
207std::string TabManager::OpenFileInTab(const std::string& filePath)
208{
209 if (filePath.empty())
210 return "";
211
212 // Check if already open
213 for (size_t i = 0; i < m_tabs.size(); ++i)
214 {
215 if (m_tabs[i].filePath == filePath)
216 {
217 SetActiveTab(m_tabs[i].tabID);
218 //SYSTEM_LOG << "[TabManager] File already open, activating: " << filePath << "\n";
219 return m_tabs[i].tabID;
220 }
221 }
222
223 std::string graphType = DetectGraphType(filePath);
224 //SYSTEM_LOG << "[TabManager] Opening file: " << filePath << " (type=" << graphType << ")\n";
225
227 tab.tabID = NextTabID();
228 tab.displayName = DisplayNameFromPath(filePath);
229 tab.filePath = filePath;
230 tab.graphType = graphType;
231 tab.isDirty = false;
232 tab.isActive = false;
233
234 if (graphType == "VisualScript")
235 {
237 if (!r->Load(filePath))
238 {
239 delete r;
240 //SYSTEM_LOG << "[TabManager] Failed to load VS file: " << filePath << "\n";
241 return "";
242 }
243 tab.renderer = r;
244 }
245 else if (graphType == "BehaviorTree")
246 {
247 // BehaviorTreeRenderer needs a NodeGraphPanel reference.
248 // The editor is single-threaded, so the static is safe here.
249 // All BT tabs share the same backing panel since they rely on the
250 // singleton NodeGraphManager; each renderer switches the active graph.
252 static bool s_btPanelInit = false;
253 if (!s_btPanelInit)
254 {
256 s_btPanelInit = true;
257 }
258
260 if (!r->Load(filePath))
261 {
262 delete r;
263 //SYSTEM_LOG << "[TabManager] Failed to load BT file: " << filePath << "\n";
264 return "";
265 }
266 tab.renderer = r;
267 }
268 else if (graphType == "EntityPrefab")
269 {
270 // EntityPrefabRenderer needs a PrefabCanvas reference.
271 // Similar to BehaviorTree, all EP tabs share the same canvas.
274 static bool s_epCanvasInit = false;
275 if (!s_epCanvasInit)
276 {
278 s_epCanvasInit = true;
279 }
280
282 if (!r->Load(filePath))
283 {
284 delete r;
285 //SYSTEM_LOG << "[TabManager] Failed to load EntityPrefab file: " << filePath << "\n";
286 return "";
287 }
288 tab.renderer = r;
289 }
290 else
291 {
292 // Fallback: try as VisualScript
294 if (!r->Load(filePath))
295 {
296 delete r;
297 //SYSTEM_LOG << "[TabManager] Unsupported/unknown graph type for: " << filePath << "\n";
298 return "";
299 }
300 tab.graphType = "VisualScript";
301 tab.renderer = r;
302 }
303
304 m_tabs.push_back(tab);
305 SetActiveTab(tab.tabID);
306 return tab.tabID;
307}
308
309// ============================================================================
310// Navigation
311// ============================================================================
312
313void TabManager::SetActiveTab(const std::string& tabID)
314{
315 // Phase 35.0: Save previous tab's canvas state before switching
317 if (previousTab && previousTab->renderer)
318 {
319 previousTab->renderer->SaveCanvasState();
320 }
321
322 // Update active tab
323 for (size_t i = 0; i < m_tabs.size(); ++i)
324 m_tabs[i].isActive = (m_tabs[i].tabID == tabID);
325 m_activeTabID = tabID;
326
327 // Request a one-shot programmatic selection for this tab.
328 // The flag is consumed (cleared) during the very next RenderTabBar() call
329 // so that subsequent user-initiated tab clicks are not overridden.
330 m_pendingSelectTabID = tabID;
331
332 // Phase 35.0: Restore new tab's canvas state after switching
334 if (newTab && newTab->renderer)
335 {
336 newTab->renderer->RestoreCanvasState();
337 }
338}
339
340std::string TabManager::GetActiveTabID() const
341{
342 return m_activeTabID;
343}
344
346{
347 for (size_t i = 0; i < m_tabs.size(); ++i)
348 {
349 if (m_tabs[i].tabID == m_activeTabID)
350 return &m_tabs[i];
351 }
352 return nullptr;
353}
354
355EditorTab* TabManager::GetTab(const std::string& tabID)
356{
357 for (size_t i = 0; i < m_tabs.size(); ++i)
358 {
359 if (m_tabs[i].tabID == tabID)
360 return &m_tabs[i];
361 }
362 return nullptr;
363}
364
365// ============================================================================
366// Closing
367// ============================================================================
368
370{
371 if (index >= m_tabs.size())
372 return;
373
374 const std::string closedID = m_tabs[index].tabID;
375 delete m_tabs[index].renderer;
376 m_tabs[index].renderer = nullptr;
377 m_tabs.erase(m_tabs.begin() + static_cast<std::vector<EditorTab>::difference_type>(index));
378
379 // Update active tab
380 if (m_activeTabID == closedID)
381 {
382 if (!m_tabs.empty())
383 {
384 size_t newActive = (index < m_tabs.size()) ? index : m_tabs.size() - 1;
386 m_tabs[newActive].isActive = true;
387 }
388 else
389 {
390 m_activeTabID = "";
391 }
392 }
393}
394
395bool TabManager::CloseTab(const std::string& tabID)
396{
397 for (size_t i = 0; i < m_tabs.size(); ++i)
398 {
399 if (m_tabs[i].tabID != tabID)
400 continue;
401
402 // Sync dirty flag from renderer
403 if (m_tabs[i].renderer)
404 m_tabs[i].isDirty = m_tabs[i].renderer->IsDirty();
405
406 if (m_tabs[i].isDirty)
407 {
408 // Dirty tabs get a deferred modal handled by RenderTabBar().
409 // Set m_pendingCloseTabID so the modal fires on the next frame.
410 if (m_pendingCloseTabID.empty())
411 m_pendingCloseTabID = tabID;
412 return false; // Actual close deferred to modal callback
413 }
414
415 DestroyTab(i);
416 return true;
417 }
418 return true;
419}
420
422{
423 // Iterate from back to front so indices don't shift
424 for (int i = static_cast<int>(m_tabs.size()) - 1; i >= 0; --i)
425 {
426 if (m_tabs[i].renderer)
427 m_tabs[i].isDirty = m_tabs[i].renderer->IsDirty();
428
429 if (m_tabs[i].isDirty)
430 {
431 SetActiveTab(m_tabs[i].tabID);
433 if (choice == 1)
434 {
435 if (!SaveActiveTabAs(m_tabs[i].filePath))
436 return false;
437 }
438 else if (choice == -1)
439 {
440 return false;
441 }
442 }
443
444 DestroyTab(static_cast<size_t>(i));
445 }
446 return true;
447}
448
449// ============================================================================
450// Saving
451// ============================================================================
452
454{
456 if (!tab || !tab->renderer)
457 return false;
458
459 if (tab->filePath.empty())
460 {
461 // Need Save As
462 m_showSaveAsDialog = true;
463 m_saveAsTabID = tab->tabID;
464 strncpy_s(m_saveAsBuffer, sizeof(m_saveAsBuffer), tab->displayName.c_str(), _TRUNCATE);
465 m_saveAsBuffer[sizeof(m_saveAsBuffer) - 1] = '\0';
466 return false; // Will complete when dialog is confirmed
467 }
468
469 //SYSTEM_LOG << "[TabManager] SaveActiveTab: saving tab '" << tab->displayName
470 // << "' to '" << tab->filePath << "'\n";
471 bool ok = tab->renderer->Save(tab->filePath);
472 if (ok)
473 {
474 tab->isDirty = false;
475 tab->displayName = DisplayNameFromPath(tab->filePath);
476 //SYSTEM_LOG << "[TabManager] SaveActiveTab: succeeded for '" << tab->filePath << "'\n";
477 }
478 else
479 {
480 //SYSTEM_LOG << "[TabManager] SaveActiveTab: FAILED for '" << tab->filePath << "'\n";
481 }
482 return ok;
483}
484
485bool TabManager::SaveActiveTabAs(const std::string& path)
486{
488 if (!tab || !tab->renderer)
489 return false;
490
491 if (path.empty())
492 {
493 // Show save-as dialog
494 m_showSaveAsDialog = true;
495 m_saveAsTabID = tab->tabID;
496 strncpy_s(m_saveAsBuffer, sizeof(m_saveAsBuffer), tab->displayName.c_str(), _TRUNCATE);
497 m_saveAsBuffer[sizeof(m_saveAsBuffer) - 1] = '\0';
498 return false;
499 }
500
501 bool ok = tab->renderer->Save(path);
502 if (ok)
503 {
504 tab->filePath = path;
505 tab->isDirty = false;
506 tab->displayName = DisplayNameFromPath(path);
507 }
508 return ok;
509}
510
511// ============================================================================
512// Query
513// ============================================================================
514
516{
517 for (size_t i = 0; i < m_tabs.size(); ++i)
518 {
519 if (m_tabs[i].isDirty)
520 return true;
521 if (m_tabs[i].renderer && m_tabs[i].renderer->IsDirty())
522 return true;
523 }
524 return false;
525}
526
527// ============================================================================
528// Rendering
529// ============================================================================
530
532{
533 // --- Unsaved dialog (deferred) ---
534 if (!m_pendingCloseTabID.empty())
535 {
536 ImGui::OpenPopup("TabManager_UnsavedClose");
537 }
538
539 if (ImGui::BeginPopupModal("TabManager_UnsavedClose", nullptr,
541 {
543 if (tab)
544 {
545 ImGui::Text("The graph \"%s\" has unsaved changes.",
546 tab->displayName.c_str());
547 ImGui::Text("Do you want to save before closing?");
548 ImGui::Separator();
549
550 if (ImGui::Button("Save", ImVec2(100, 0)))
551 {
554 // Close after saving
555 for (size_t i = 0; i < m_tabs.size(); ++i)
556 {
557 if (m_tabs[i].tabID == m_pendingCloseTabID)
558 {
559 DestroyTab(i);
560 break;
561 }
562 }
564 ImGui::CloseCurrentPopup();
565 }
566 ImGui::SameLine();
567 if (ImGui::Button("Don't Save", ImVec2(100, 0)))
568 {
569 for (size_t i = 0; i < m_tabs.size(); ++i)
570 {
571 if (m_tabs[i].tabID == m_pendingCloseTabID)
572 {
573 DestroyTab(i);
574 break;
575 }
576 }
578 ImGui::CloseCurrentPopup();
579 }
580 ImGui::SameLine();
581 if (ImGui::Button("Cancel", ImVec2(100, 0)))
582 {
584 ImGui::CloseCurrentPopup();
585 }
586 }
587 else
588 {
590 ImGui::CloseCurrentPopup();
591 }
592
593 ImGui::EndPopup();
594 }
595
596 // --- Tab bar ---
597 if (m_tabs.empty())
598 {
599 ImGui::TextDisabled("No graph open. Double-click a file to open it.");
600 return;
601 }
602
605
606 if (ImGui::BeginTabBar("GraphTabBar", tabBarFlags))
607 {
608 for (size_t i = 0; i < m_tabs.size(); ++i)
609 {
610 EditorTab& tab = m_tabs[i];
611
612 // Sync dirty flag from renderer
613 if (tab.renderer)
614 tab.isDirty = tab.renderer->IsDirty();
615
616 // Sync file path and display name from renderer.
617 // This covers the case where the panel's own Save/Save As dialog
618 // was used directly (not via TabManager::SaveActiveTab).
619 if (tab.renderer)
620 {
621 std::string rendererPath = tab.renderer->GetCurrentPath();
622 if (!rendererPath.empty() && rendererPath != tab.filePath)
623 {
624 tab.filePath = rendererPath;
625 tab.displayName = DisplayNameFromPath(rendererPath);
626 }
627 }
628
629 // Build label: "name *" when dirty
630 std::string label = tab.displayName;
631 if (tab.isDirty)
632 label += " *";
633 label += "###tab_";
634 label += tab.tabID;
635
636 // Apply ImGuiTabItemFlags_SetSelected only once (one-shot) when
637 // this tab was programmatically activated. The flag is cleared
638 // immediately after use so that subsequent user-initiated tab
639 // clicks are not suppressed.
641 if (tab.tabID == m_pendingSelectTabID)
642 {
644 m_pendingSelectTabID = ""; // consume the one-shot request
645 }
646
647 // Close button
648 bool open = true;
649 if (ImGui::BeginTabItem(label.c_str(), &open, flags))
650 {
651 if (m_activeTabID != tab.tabID)
652 SetActiveTab(tab.tabID);
653 ImGui::EndTabItem();
654 }
655
656 // Handle close via X button
657 if (!open && m_pendingCloseTabID.empty())
658 {
659 if (tab.isDirty)
660 {
661 // Defer to modal dialog
662 m_pendingCloseTabID = tab.tabID;
663 }
664 else
665 {
666 DestroyTab(i);
667 break; // m_tabs may be smaller now
668 }
669 }
670 }
671
672 // "+" button for new VS graph
673 if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing |
675 {
676 CreateNewTab("VisualScript");
677 }
678
679 ImGui::EndTabBar();
680 }
681}
682
684{
686 if (!tab || !tab->renderer)
687 {
688 ImGui::TextDisabled("No graph open.");
689 return;
690 }
691 tab->renderer->Render();
692}
693
694// ============================================================================
695// Dialogs
696// ============================================================================
697
699{
700 // NOTE: ImGui is immediate-mode; true blocking modal dialogs cannot be
701 // shown outside a render frame. Interactive unsaved-change prompts are
702 // handled by the deferred modal inside RenderTabBar() (triggered by the
703 // [X] close button).
704 //
705 // This fallback is only reached by CloseAllTabs(), which is called from
706 // keyboard shortcuts and File > Close All. In that context there is no
707 // safe way to block for user input, so we discard unsaved changes.
708 // CloseTab() (single tab) always uses the deferred RenderTabBar modal and
709 // therefore never calls this method for dirty tabs.
710 (void)tab;
711 return 0; // Don't Save
712}
713
714} // namespace Olympe
IGraphRenderer adapter for BehaviorTree graphs (wraps BTNodeGraphManager).
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static SDL_Renderer * renderer
Central manager for the multi-graph tab system.
IGraphRenderer adapter that wraps VisualScriptEditorPanel.
Adapts the BTNodeGraphManager + NodeGraphPanel to IGraphRenderer.
Renderer adapter for Entity Prefab graphs.
NodeGraphPanel - ImGui/ImNodes panel for node graph editing Provides visual editor for behavior trees...
void Initialize(EntityPrefabGraphDocument *document)
Singleton that owns and manages all open graph editor tabs.
Definition TabManager.h:65
int m_nextTabNum
Counter for "Untitled-N" names.
Definition TabManager.h:190
bool SaveActiveTabAs(const std::string &path)
Saves the active tab to a specific path.
bool HasDirtyTabs() const
Returns true when at least one tab has unsaved changes.
int ShowUnsavedDialog(const EditorTab &tab)
Shows a 3-button "Save / Don't Save / Cancel" modal dialog.
static TabManager & Get()
Returns the global singleton instance.
EditorTab * GetActiveTab()
EditorTab * GetTab(const std::string &tabID)
std::string m_saveAsTabID
Definition TabManager.h:204
std::string GetActiveTabID() const
std::string CreateNewTab(const std::string &graphType)
Creates a new empty tab of the given graph type.
std::string NextTabID()
Generates the next unique tabID.
bool CloseAllTabs()
Attempts to close all tabs.
bool SaveActiveTab()
Saves the active tab.
bool CloseTab(const std::string &tabID)
Closes the given tab.
static std::string DisplayNameFromPath(const std::string &filePath)
Derives a display name from a file path (filename without dir).
int m_nextTabIDNum
Counter for unique tab IDs.
Definition TabManager.h:191
static std::string DetectGraphType(const std::string &filePath)
Detects the graph type by inspecting the JSON contents.
void RenderTabBar()
Renders the horizontal tab bar (call before RenderActiveCanvas).
std::vector< EditorTab > m_tabs
Definition TabManager.h:188
std::string m_pendingSelectTabID
Definition TabManager.h:196
void RenderActiveCanvas()
Renders the graph canvas of the active tab.
char m_saveAsBuffer[512]
Definition TabManager.h:203
void DestroyTab(size_t index)
Deletes a renderer and removes its tab entry.
std::string m_pendingCloseTabID
Definition TabManager.h:199
void SetActiveTab(const std::string &tabID)
std::string OpenFileInTab(const std::string &filePath)
Opens a file in a new tab.
std::string m_activeTabID
Definition TabManager.h:189
Adapts the existing VisualScriptEditorPanel to the IGraphRenderer interface.
< Provides AssetID and INVALID_ASSET_ID
nlohmann::json json
Represents a single open graph in the editor.
Definition TabManager.h:39
bool isDirty
True when there are unsaved changes.
Definition TabManager.h:44
std::string tabID
Unique identifier (stringified int counter)
Definition TabManager.h:40