Add ImGui Scene Explorer (F4) for runtime object inspection (#136)
New Features: - Scene Explorer window (F4) displays hierarchical tree of all scenes - Shows UIDrawable hierarchy with type, name, and visibility status - Click scene name to switch active scene - Double-click drawables to toggle visibility - Displays Python repr() for cached objects, enabling custom class debugging - Entity display within Grid nodes Bug Fixes: - Fix PythonObjectCache re-registration: when retrieving objects from collections, newly created Python wrappers are now re-registered in the cache. Previously, inline-created objects (e.g., scene.children.append(Frame(...))) would lose their cache entry when the temporary Python object was GC'd, causing repeated wrapper allocation on each access. - Fix console focus stealing: removed aggressive focus reclaim that caused title bar flashing when clicking in Scene Explorer Infrastructure: - Add GameEngine::getSceneNames() to expose scene list for explorer - Scene Explorer uses same enabled flag as console (ImGuiConsole::isEnabled()) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4be2502a10
commit
5e45ab015c
7 changed files with 378 additions and 3 deletions
|
|
@ -202,6 +202,16 @@ Scene* GameEngine::getScene(const std::string& name) {
|
|||
auto it = scenes.find(name);
|
||||
return (it != scenes.end()) ? it->second : nullptr;
|
||||
}
|
||||
|
||||
std::vector<std::string> GameEngine::getSceneNames() const {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(scenes.size());
|
||||
for (const auto& [name, scene] : scenes) {
|
||||
names.push_back(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
void GameEngine::changeScene(std::string s)
|
||||
{
|
||||
changeScene(s, TransitionType::None, 0.0f);
|
||||
|
|
@ -346,9 +356,10 @@ void GameEngine::run()
|
|||
profilerOverlay->render(*render_target);
|
||||
}
|
||||
|
||||
// Render ImGui console overlay
|
||||
// Render ImGui overlays (console and scene explorer)
|
||||
if (imguiInitialized && !headless) {
|
||||
console.render();
|
||||
sceneExplorer.render(*this);
|
||||
ImGui::SFML::Render(*window);
|
||||
}
|
||||
|
||||
|
|
@ -550,6 +561,12 @@ void GameEngine::sUserInput()
|
|||
continue; // Don't pass grave key to game
|
||||
}
|
||||
|
||||
// Handle F4 for scene explorer toggle
|
||||
if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F4) {
|
||||
sceneExplorer.toggle();
|
||||
continue; // Don't pass F4 to game
|
||||
}
|
||||
|
||||
// If console wants keyboard, don't pass keyboard events to game
|
||||
if (console.wantsKeyboardInput()) {
|
||||
// Still process non-keyboard events (mouse, window close, etc.)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "SceneTransition.h"
|
||||
#include "Profiler.h"
|
||||
#include "ImGuiConsole.h"
|
||||
#include "ImGuiSceneExplorer.h"
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <mutex>
|
||||
|
|
@ -191,6 +192,7 @@ private:
|
|||
|
||||
// ImGui console overlay
|
||||
ImGuiConsole console;
|
||||
ImGuiSceneExplorer sceneExplorer;
|
||||
bool imguiInitialized = false;
|
||||
|
||||
// #219 - Thread synchronization for background Python threads
|
||||
|
|
@ -214,6 +216,7 @@ public:
|
|||
~GameEngine();
|
||||
Scene* currentScene();
|
||||
Scene* getScene(const std::string& name); // #118: Get scene by name
|
||||
std::vector<std::string> getSceneNames() const; // #136: Get all scene names for explorer
|
||||
void changeScene(std::string);
|
||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
||||
void createScene(std::string);
|
||||
|
|
|
|||
|
|
@ -381,9 +381,9 @@ void ImGuiConsole::render() {
|
|||
}
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
// Keep focus on input
|
||||
// Keep focus on input only after executing a command
|
||||
ImGui::SetItemDefaultFocus();
|
||||
if (reclaimFocus || (visible && !ImGui::IsAnyItemActive())) {
|
||||
if (reclaimFocus) {
|
||||
ImGui::SetKeyboardFocusHere(-1);
|
||||
}
|
||||
|
||||
|
|
|
|||
285
src/ImGuiSceneExplorer.cpp
Normal file
285
src/ImGuiSceneExplorer.cpp
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
#include "ImGuiSceneExplorer.h"
|
||||
#include "imgui.h"
|
||||
#include "GameEngine.h"
|
||||
#include "Scene.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIEntity.h"
|
||||
#include "ImGuiConsole.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <vector>
|
||||
|
||||
bool ImGuiSceneExplorer::isEnabled() {
|
||||
// Use the same enabled flag as the console
|
||||
return ImGuiConsole::isEnabled();
|
||||
}
|
||||
|
||||
void ImGuiSceneExplorer::toggle() {
|
||||
if (isEnabled()) {
|
||||
visible = !visible;
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiSceneExplorer::render(GameEngine& engine) {
|
||||
if (!visible || !isEnabled()) return;
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Position on the right side of the screen
|
||||
ImGui::SetNextWindowSize(ImVec2(350, io.DisplaySize.y * 0.6f), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - 360, 10), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
|
||||
if (!ImGui::Begin("Scene Explorer", &visible, flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// Scene tree header
|
||||
ImGui::Text("Scenes (%zu):", engine.getSceneNames().size());
|
||||
ImGui::Separator();
|
||||
|
||||
// Scrollable tree region
|
||||
ImGui::BeginChild("SceneTree", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
|
||||
// Get all scene names and render each
|
||||
std::string currentSceneName = engine.scene;
|
||||
std::vector<std::string> sceneNames = engine.getSceneNames();
|
||||
|
||||
for (const auto& sceneName : sceneNames) {
|
||||
bool isActive = (sceneName == currentSceneName);
|
||||
renderSceneNode(engine, sceneName, isActive);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ImGuiSceneExplorer::renderSceneNode(GameEngine& engine, const std::string& sceneName, bool isActive) {
|
||||
ImGuiTreeNodeFlags sceneFlags = ImGuiTreeNodeFlags_OpenOnArrow
|
||||
| ImGuiTreeNodeFlags_OpenOnDoubleClick
|
||||
| ImGuiTreeNodeFlags_DefaultOpen;
|
||||
|
||||
if (isActive) {
|
||||
sceneFlags |= ImGuiTreeNodeFlags_Selected;
|
||||
}
|
||||
|
||||
// Build label with active indicator
|
||||
std::string label = sceneName;
|
||||
if (isActive) {
|
||||
label += " [active]";
|
||||
}
|
||||
|
||||
// Scene icon/indicator
|
||||
bool sceneOpen = ImGui::TreeNodeEx(("##scene_" + sceneName).c_str(), sceneFlags, "%s %s",
|
||||
isActive ? ">" : " ", label.c_str());
|
||||
|
||||
// Click to activate scene (if not already active)
|
||||
if (ImGui::IsItemClicked() && !isActive) {
|
||||
engine.changeScene(sceneName);
|
||||
}
|
||||
|
||||
if (sceneOpen) {
|
||||
// Get scene's UI elements
|
||||
auto ui_elements = engine.scene_ui(sceneName);
|
||||
if (ui_elements && !ui_elements->empty()) {
|
||||
for (auto& drawable : *ui_elements) {
|
||||
if (drawable) {
|
||||
renderDrawableNode(drawable, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ImGui::TextDisabled(" (empty)");
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiSceneExplorer::renderDrawableNode(std::shared_ptr<UIDrawable> drawable, int depth) {
|
||||
if (!drawable) return;
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow
|
||||
| ImGuiTreeNodeFlags_OpenOnDoubleClick
|
||||
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||
|
||||
// Check if this node has children
|
||||
bool hasChildren = false;
|
||||
UIFrame* frame = nullptr;
|
||||
UIGrid* grid = nullptr;
|
||||
|
||||
switch (drawable->derived_type()) {
|
||||
case PyObjectsEnum::UIFRAME:
|
||||
frame = static_cast<UIFrame*>(drawable.get());
|
||||
hasChildren = frame->children && !frame->children->empty();
|
||||
break;
|
||||
case PyObjectsEnum::UIGRID:
|
||||
grid = static_cast<UIGrid*>(drawable.get());
|
||||
hasChildren = (grid->entities && !grid->entities->empty()) ||
|
||||
(grid->children && !grid->children->empty());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!hasChildren) {
|
||||
flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
||||
}
|
||||
|
||||
// Visibility indicator
|
||||
const char* visIcon = drawable->visible ? "[v]" : "[h]";
|
||||
|
||||
// Build display string
|
||||
std::string displayName = getDisplayName(drawable.get());
|
||||
std::string nodeLabel = std::string(visIcon) + " " + getTypeName(drawable.get()) + ": " + displayName;
|
||||
|
||||
// Use pointer as unique ID
|
||||
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)drawable.get(), flags, "%s", nodeLabel.c_str());
|
||||
|
||||
// Double-click to toggle visibility
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
drawable->visible = !drawable->visible;
|
||||
}
|
||||
|
||||
// Handle leaf nodes (NoTreePushOnOpen means TreePop not needed)
|
||||
if (!hasChildren) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeOpen) {
|
||||
// Render children based on type
|
||||
if (frame && frame->children) {
|
||||
for (auto& child : *frame->children) {
|
||||
if (child) {
|
||||
renderDrawableNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (grid) {
|
||||
// Render entities
|
||||
if (grid->entities && !grid->entities->empty()) {
|
||||
ImGuiTreeNodeFlags entityGroupFlags = ImGuiTreeNodeFlags_OpenOnArrow;
|
||||
bool entitiesOpen = ImGui::TreeNodeEx("Entities", entityGroupFlags, "Entities (%zu)",
|
||||
grid->entities->size());
|
||||
if (entitiesOpen) {
|
||||
for (auto& entity : *grid->entities) {
|
||||
if (entity) {
|
||||
renderEntityNode(entity);
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
// Render grid's drawable children (overlays)
|
||||
if (grid->children && !grid->children->empty()) {
|
||||
ImGuiTreeNodeFlags overlayGroupFlags = ImGuiTreeNodeFlags_OpenOnArrow;
|
||||
bool overlaysOpen = ImGui::TreeNodeEx("Overlays", overlayGroupFlags, "Overlays (%zu)",
|
||||
grid->children->size());
|
||||
if (overlaysOpen) {
|
||||
for (auto& child : *grid->children) {
|
||||
if (child) {
|
||||
renderDrawableNode(child, depth + 1);
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiSceneExplorer::renderEntityNode(std::shared_ptr<UIEntity> entity) {
|
||||
if (!entity) return;
|
||||
|
||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf
|
||||
| ImGuiTreeNodeFlags_NoTreePushOnOpen
|
||||
| ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||
|
||||
std::string displayName = getEntityDisplayName(entity.get());
|
||||
std::string nodeLabel = "Entity: " + displayName;
|
||||
|
||||
ImGui::TreeNodeEx((void*)(intptr_t)entity.get(), flags, "%s", nodeLabel.c_str());
|
||||
}
|
||||
|
||||
std::string ImGuiSceneExplorer::getDisplayName(UIDrawable* drawable) {
|
||||
if (!drawable) return "(null)";
|
||||
|
||||
// Try to get Python object repr from cache
|
||||
if (drawable->serial_number != 0) {
|
||||
PyObject* pyObj = PythonObjectCache::getInstance().lookup(drawable->serial_number);
|
||||
if (pyObj) {
|
||||
PyObject* repr = PyObject_Repr(pyObj);
|
||||
if (repr) {
|
||||
const char* repr_str = PyUnicode_AsUTF8(repr);
|
||||
if (repr_str) {
|
||||
std::string result(repr_str);
|
||||
Py_DECREF(repr);
|
||||
Py_DECREF(pyObj);
|
||||
return result;
|
||||
}
|
||||
Py_DECREF(repr);
|
||||
}
|
||||
Py_DECREF(pyObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Use name if available
|
||||
if (!drawable->name.empty()) {
|
||||
return "\"" + drawable->name + "\"";
|
||||
}
|
||||
|
||||
// Fall back to address
|
||||
std::ostringstream oss;
|
||||
oss << "@" << std::hex << std::setw(8) << std::setfill('0') << (uintptr_t)drawable;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string ImGuiSceneExplorer::getEntityDisplayName(UIEntity* entity) {
|
||||
if (!entity) return "(null)";
|
||||
|
||||
// Try to get Python object repr from cache
|
||||
if (entity->serial_number != 0) {
|
||||
PyObject* pyObj = PythonObjectCache::getInstance().lookup(entity->serial_number);
|
||||
if (pyObj) {
|
||||
PyObject* repr = PyObject_Repr(pyObj);
|
||||
if (repr) {
|
||||
const char* repr_str = PyUnicode_AsUTF8(repr);
|
||||
if (repr_str) {
|
||||
std::string result(repr_str);
|
||||
Py_DECREF(repr);
|
||||
Py_DECREF(pyObj);
|
||||
return result;
|
||||
}
|
||||
Py_DECREF(repr);
|
||||
}
|
||||
Py_DECREF(pyObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to position
|
||||
std::ostringstream oss;
|
||||
oss << "(" << entity->position.x << ", " << entity->position.y << ")";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
const char* ImGuiSceneExplorer::getTypeName(UIDrawable* drawable) {
|
||||
if (!drawable) return "null";
|
||||
|
||||
switch (drawable->derived_type()) {
|
||||
case PyObjectsEnum::UIFRAME: return "Frame";
|
||||
case PyObjectsEnum::UICAPTION: return "Caption";
|
||||
case PyObjectsEnum::UISPRITE: return "Sprite";
|
||||
case PyObjectsEnum::UIGRID: return "Grid";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
46
src/ImGuiSceneExplorer.h
Normal file
46
src/ImGuiSceneExplorer.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
class GameEngine;
|
||||
class UIDrawable;
|
||||
class UIEntity;
|
||||
class UIFrame;
|
||||
class UIGrid;
|
||||
|
||||
/**
|
||||
* @brief ImGui-based scene tree explorer for debugging
|
||||
*
|
||||
* Displays hierarchical view of all scenes and their UI elements.
|
||||
* Allows switching between scenes and collapsing/expanding the tree.
|
||||
* Activated by F4 key. Mutually exclusive with the console (grave key).
|
||||
*/
|
||||
class ImGuiSceneExplorer {
|
||||
public:
|
||||
ImGuiSceneExplorer() = default;
|
||||
|
||||
// Core functionality
|
||||
void render(GameEngine& engine);
|
||||
void toggle();
|
||||
bool isVisible() const { return visible; }
|
||||
void setVisible(bool v) { visible = v; }
|
||||
|
||||
// Configuration - uses same enabled flag as console
|
||||
static bool isEnabled();
|
||||
|
||||
private:
|
||||
bool visible = false;
|
||||
|
||||
// Tree rendering helpers
|
||||
void renderSceneNode(GameEngine& engine, const std::string& sceneName, bool isActive);
|
||||
void renderDrawableNode(std::shared_ptr<UIDrawable> drawable, int depth = 0);
|
||||
void renderEntityNode(std::shared_ptr<UIEntity> entity);
|
||||
|
||||
// Get display name for a drawable (name or type + address)
|
||||
std::string getDisplayName(UIDrawable* drawable);
|
||||
std::string getEntityDisplayName(UIEntity* entity);
|
||||
|
||||
// Get type name string
|
||||
const char* getTypeName(UIDrawable* drawable);
|
||||
};
|
||||
|
|
@ -126,6 +126,18 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
if (type) {
|
||||
Py_DECREF(type);
|
||||
}
|
||||
|
||||
// Re-register in cache if the object has a serial number
|
||||
// This handles the case where the original Python wrapper was GC'd
|
||||
// but the C++ object persists (e.g., inline-created objects added to collections)
|
||||
if (obj && drawable->serial_number != 0) {
|
||||
PyObject* weakref = PyWeakref_NewRef(obj, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(drawable->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,18 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
|
|||
|
||||
o->data = std::static_pointer_cast<UIEntity>(target);
|
||||
o->weakreflist = NULL;
|
||||
|
||||
// Re-register in cache if the entity has a serial number
|
||||
// This handles the case where the original Python wrapper was GC'd
|
||||
// but the C++ object persists (e.g., inline-created objects added to collections)
|
||||
if (target->serial_number != 0) {
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)o, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(target->serial_number, weakref);
|
||||
Py_DECREF(weakref);
|
||||
}
|
||||
}
|
||||
|
||||
return (PyObject*)o;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue