diff --git a/resources/icons/file-plus.svg b/resources/icons/file-plus.svg new file mode 100644 index 0000000..c96900c --- /dev/null +++ b/resources/icons/file-plus.svg @@ -0,0 +1 @@ + diff --git a/resources/icons/pen.svg b/resources/icons/pen.svg new file mode 100644 index 0000000..408a9e2 --- /dev/null +++ b/resources/icons/pen.svg @@ -0,0 +1 @@ + diff --git a/src/core/LayerManager.cpp b/src/core/LayerManager.cpp index e051400..bdbd74f 100644 --- a/src/core/LayerManager.cpp +++ b/src/core/LayerManager.cpp @@ -148,6 +148,15 @@ void LayerManager::UnmuteLayer(const std::string& layerIdentifier) { } } +bool LayerManager::SetEditTarget(const std::string& identifier) { + if (!m_stage) return false; + SdfLayerHandle layer = SdfLayer::Find(identifier); + if (!layer) return false; + m_stage->SetEditTarget(layer); + Refresh(); + return true; +} + bool LayerManager::IsLayerMuted(const std::string& layerIdentifier) const { if (m_stage) { return m_stage->IsLayerMuted(layerIdentifier); @@ -181,32 +190,51 @@ std::string LayerManager::ExtractDisplayName(const std::string& identifier) { void LayerManager::BuildLayerList() { m_layers.clear(); - + m_layerRefs.clear(); if (!m_stage) return; - + try { - // Get the full layer stack (root + all sublayers) - SdfLayerHandleVector layerStack = m_stage->GetLayerStack(); - - SdfLayerHandle rootLayer = m_stage->GetRootLayer(); + SdfLayerHandle rootLayer = m_stage->GetRootLayer(); SdfLayerHandle sessionLayer = m_stage->GetSessionLayer(); - - for (const auto& layer : layerStack) { + SdfLayerHandle editTarget = m_stage->GetEditTarget().GetLayer(); + + auto makeInfo = [&](SdfLayerHandle layer, bool isRoot, bool isSession) { LayerInfo info; - info.layer = layer; - info.identifier = layer->GetIdentifier(); - info.displayName = ExtractDisplayName(info.identifier); - info.realPath = layer->GetRealPath(); - info.isMuted = layer->IsMuted(); - info.isAnonymous = layer->IsAnonymous(); - info.isRootLayer = (layer == rootLayer); - info.isSessionLayer = (layer == sessionLayer); - - m_layers.push_back(info); + info.layer = layer; + info.identifier = layer->GetIdentifier(); + info.displayName = ExtractDisplayName(info.identifier); + info.realPath = layer->GetRealPath(); + info.isMuted = m_stage->IsLayerMuted(info.identifier); + info.isAnonymous = layer->IsAnonymous(); + info.isRootLayer = isRoot; + info.isSessionLayer = isSession; + info.isEditTarget = (layer == editTarget); + info.isDirty = layer->IsDirty(); + return info; + }; + + // Root and session layers: stage always holds strong refs, handles are valid. + if (sessionLayer) m_layers.push_back(makeInfo(sessionLayer, false, true)); + if (rootLayer) m_layers.push_back(makeInfo(rootLayer, true, false)); + + // Walk root layer's sublayer paths instead of stage->GetLayerStack(). + // GetLayerStack() drops muted layers because they don't contribute to + // composition. FindOrOpenRelativeToLayer locates or reopens the layer + // regardless of mute state. We store the returned RefPtr in m_layerRefs + // so the layer object isn't garbage-collected before the next Refresh(). + if (rootLayer) { + for (const auto& subPath : rootLayer->GetSubLayerPaths()) { + SdfLayerRefPtr subRef = + SdfLayer::FindOrOpenRelativeToLayer(rootLayer, subPath); + if (subRef) { + m_layerRefs.push_back(subRef); + m_layers.push_back(makeInfo(SdfLayerHandle(subRef), false, false)); + } + } } - + LOG_DEBUG("Built layer list with " + std::to_string(m_layers.size()) + " layers"); - + } catch (const std::exception& e) { LOG_ERROR(std::string("Failed to build layer list: ") + e.what()); } diff --git a/src/core/LayerManager.h b/src/core/LayerManager.h index fc825e0..c84e37a 100644 --- a/src/core/LayerManager.h +++ b/src/core/LayerManager.h @@ -17,6 +17,8 @@ struct LayerInfo { bool isAnonymous; bool isRootLayer; bool isSessionLayer; + bool isEditTarget; + bool isDirty; }; class LayerManager { @@ -39,6 +41,9 @@ public: bool MoveSublayerUp(int index); bool MoveSublayerDown(int index); + // Edit target + bool SetEditTarget(const std::string& identifier); + // Muting void MuteLayer(const std::string& layerIdentifier); void UnmuteLayer(const std::string& layerIdentifier); @@ -49,9 +54,11 @@ public: private: void BuildLayerList(); - + pxr::UsdStageRefPtr m_stage; std::vector m_layers; + // Strong references keep sublayers alive even when the stage drops them after muting. + std::vector m_layerRefs; }; } // namespace UsdLayerManager \ No newline at end of file diff --git a/src/core/commands/LayerCommands.cpp b/src/core/commands/LayerCommands.cpp index 67475e7..5361484 100644 --- a/src/core/commands/LayerCommands.cpp +++ b/src/core/commands/LayerCommands.cpp @@ -46,10 +46,10 @@ LayerRemoveCommand::LayerRemoveCommand(LayerManager* mgr, int index) , m_description("Remove Layer") { if (!mgr) return; - auto layers = mgr->GetLayerStack(); - if (index >= 0 && index < static_cast(layers.size())) { - m_savedPath = layers[index].identifier; - m_description = "Remove Layer " + layers[index].displayName; + auto sublayers = mgr->GetSublayers(); + if (index >= 0 && index < static_cast(sublayers.size())) { + m_savedPath = sublayers[index].identifier; + m_description = "Remove Layer " + sublayers[index].displayName; } } @@ -81,22 +81,14 @@ void LayerReorderCommand::Undo() { ApplyOrder(m_before); } void LayerReorderCommand::ApplyOrder(const std::vector& order) { if (!m_mgr) return; - // Remove all sublayers and re-insert in the desired order. - // We only control sublayers — root and session are fixed. - // First gather current sublayer indices (non-root, non-session). - auto layers = m_mgr->GetLayerStack(); - // Count sublayers and remove them from highest index down. - std::vector sublayerIndices; - for (int i = 0; i < static_cast(layers.size()); ++i) { - if (!layers[i].isRootLayer && !layers[i].isSessionLayer) - sublayerIndices.push_back(i); - } - // Remove from back to front to preserve indices. - for (int i = static_cast(sublayerIndices.size()) - 1; i >= 0; --i) - m_mgr->RemoveSublayer(sublayerIndices[i]); + // RemoveSublayer() operates on GetSubLayerPaths() — a sublayer-local list + // with indices 0..N-1. GetLayerStack() prepends session and root layers, so + // its indices are always wrong here. Use GetSublayers().size() for the count. + int count = static_cast(m_mgr->GetSublayers().size()); + for (int i = count - 1; i >= 0; --i) + m_mgr->RemoveSublayer(i); - // Re-add in desired order. for (const auto& path : order) m_mgr->InsertSublayerPath(path, -1); } diff --git a/src/ui/Application.cpp b/src/ui/Application.cpp index c693eb4..8745ee0 100644 --- a/src/ui/Application.cpp +++ b/src/ui/Application.cpp @@ -57,9 +57,9 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig m_layerManager = std::make_unique(); m_propertyManager = std::make_unique(); m_propertyManager->SetCommandHistory(&m_commandHistory); - m_layerPanel = std::make_unique(); - m_layerPanel->SetLayerManager(m_layerManager.get()); - m_layerPanel->SetCommandHistory(&m_commandHistory); + m_stageEditorPanel = std::make_unique(); + m_stageEditorPanel->SetLayerManager(m_layerManager.get()); + m_stageEditorPanel->SetCommandHistory(&m_commandHistory); m_sceneHierarchyPanel = std::make_unique(); m_sceneHierarchyPanel->SetPropertyManager(m_propertyManager.get()); m_sceneHierarchyPanel->SetCommandHistory(&m_commandHistory); @@ -82,6 +82,7 @@ bool Application::Initialize(const std::string& windowTitle, int width, int heig m_sceneHierarchyPanel->SetIconManager(m_iconManager.get()); m_viewportPanel->SetIconManager(m_iconManager.get()); m_timelinePanel->SetIconManager(m_iconManager.get()); + m_stageEditorPanel->SetIconManager(m_iconManager.get()); m_sceneHierarchyPanel->SetOnPrimSelected( [this](const std::string& path) { @@ -132,7 +133,7 @@ void Application::Shutdown() { m_viewportPanel.reset(); m_sceneHierarchyPanel.reset(); m_propertyPanel.reset(); - m_layerPanel.reset(); + m_stageEditorPanel.reset(); m_propertyManager.reset(); m_layerManager.reset(); @@ -206,8 +207,8 @@ void Application::RenderUI() { RenderStageInfo(); } - ImGui::Begin("Layer Panel", nullptr, ImGuiWindowFlags_NoCollapse); - m_layerPanel->Render(); + ImGui::Begin("Stage Editor", nullptr, ImGuiWindowFlags_NoCollapse); + m_stageEditorPanel->Render(); ImGui::End(); m_viewportPanel->Render(); diff --git a/src/ui/Application.h b/src/ui/Application.h index bb3ffed..6511dba 100644 --- a/src/ui/Application.h +++ b/src/ui/Application.h @@ -2,7 +2,7 @@ #include "ImGuiContext.h" #include "IconManager.h" -#include "LayerPanel.h" +#include "StageEditorPanel.h" #include "SceneHierarchyPanel.h" #include "ViewportPanel.h" #include "PropertyPanel.h" @@ -49,7 +49,7 @@ private: std::unique_ptr m_layerManager; std::unique_ptr m_propertyManager; CommandHistory m_commandHistory; - std::unique_ptr m_layerPanel; + std::unique_ptr m_stageEditorPanel; std::unique_ptr m_sceneHierarchyPanel; std::unique_ptr m_viewportPanel; std::unique_ptr m_propertyPanel; diff --git a/src/ui/IconManager.cpp b/src/ui/IconManager.cpp index c8b9c62..6c26576 100644 --- a/src/ui/IconManager.cpp +++ b/src/ui/IconManager.cpp @@ -57,6 +57,9 @@ static const char* IconFilename(Icon icon) { case Icon::SkipEnd: return "skip-end.svg"; case Icon::Loop: return "loop.svg"; case Icon::Bounce: return "bounce.svg"; + case Icon::Refresh: return "arrows-rotate.svg"; + case Icon::FilePlus: return "file-plus.svg"; + case Icon::Pen: return "pen.svg"; default: return nullptr; } } @@ -72,6 +75,7 @@ static constexpr Icon kAllIcons[] = { Icon::LayoutSingle, Icon::LayoutHSplit, Icon::LayoutVSplit, Icon::LayoutQuad, Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause, Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce, + Icon::Refresh, Icon::FilePlus, Icon::Pen, }; // --------------------------------------------------------------------------- diff --git a/src/ui/IconManager.h b/src/ui/IconManager.h index 1de2595..f13a899 100644 --- a/src/ui/IconManager.h +++ b/src/ui/IconManager.h @@ -50,6 +50,10 @@ enum class Icon { SkipEnd, // go to last frame (▶|) Loop, // loop toggle (↻) Bounce, // bounce/ping-pong (↔) + // General actions + Refresh, // reload / refresh + FilePlus, // create new file + Pen, // edit / edit target }; /// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as diff --git a/src/ui/LayerPanel.cpp b/src/ui/LayerPanel.cpp deleted file mode 100644 index c6d4eca..0000000 --- a/src/ui/LayerPanel.cpp +++ /dev/null @@ -1,253 +0,0 @@ -#include "LayerPanel.h" -#include "../utils/Logger.h" -#include "../core/commands/LayerCommands.h" -#include -#include - -namespace UsdLayerManager { - -LayerPanel::LayerPanel() - : m_layerManager(nullptr) - , m_selectedLayerIndex(-1) - , m_showCreateDialog(false) { - m_newLayerPath[0] = '\0'; - m_newLayerName[0] = '\0'; -} - -LayerPanel::~LayerPanel() { -} - -void LayerPanel::SetLayerManager(LayerManager* manager) { - m_layerManager = manager; -} - -void LayerPanel::Render() { - if (!m_layerManager) return; - - // Header with buttons - ImGui::Text("Layers"); - ImGui::SameLine(ImGui::GetWindowWidth() - 110); - - if (ImGui::Button("Refresh")) { - m_layerManager->Refresh(); - } - ImGui::SameLine(); - if (ImGui::Button("Add Layer")) { - m_showCreateDialog = true; - m_newLayerPath[0] = '\0'; - strcpy_s(m_newLayerName, "new_layer.usd"); - } - - ImGui::Separator(); - - // Create layer dialog - if (m_showCreateDialog) { - ShowCreateLayerDialog(); - } - - // Layer list - auto layers = m_layerManager->GetLayerStack(); - - if (layers.empty()) { - ImGui::TextDisabled("No layers loaded"); - return; - } - - // Layer table - if (ImGui::BeginTable("LayerTable", 4, - ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | - ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) { - - ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 20.0f); - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 60.0f); - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80.0f); - ImGui::TableHeadersRow(); - - for (int i = 0; i < static_cast(layers.size()); i++) { - const auto& layerInfo = layers[i]; - ImGui::TableNextRow(); - - bool isSelected = (m_selectedLayerIndex == i); - - // Selection column - ImGui::TableSetColumnIndex(0); - ImGui::PushID(i); - if (ImGui::Selectable("##select", isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap)) { - m_selectedLayerIndex = i; - } - ImGui::PopID(); - - // Name column - ImGui::TableSetColumnIndex(1); - ImVec4 textColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - if (layerInfo.isMuted) { - textColor = ImVec4(0.5f, 0.5f, 0.5f, 1.0f); - } else if (layerInfo.isRootLayer) { - textColor = ImVec4(0.5f, 1.0f, 0.5f, 1.0f); - } else if (layerInfo.isSessionLayer) { - textColor = ImVec4(0.5f, 0.7f, 1.0f, 1.0f); - } - - ImGui::TextColored(textColor, "%s", layerInfo.displayName.c_str()); - - if (ImGui::IsItemHovered() && !layerInfo.realPath.empty()) { - ImGui::SetTooltip("%s\n%s", layerInfo.identifier.c_str(), layerInfo.realPath.c_str()); - } - - // Mute toggle - ImGui::TableSetColumnIndex(2); - bool muted = layerInfo.isMuted; - ImGui::PushID(("mute_" + std::to_string(i)).c_str()); - if (ImGui::Checkbox("##muted", &muted)) { - if (muted) { - m_layerManager->MuteLayer(layerInfo.identifier); - } else { - m_layerManager->UnmuteLayer(layerInfo.identifier); - } - } - ImGui::PopID(); - - // Type column - ImGui::TableSetColumnIndex(3); - if (layerInfo.isRootLayer) { - ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Root"); - } else if (layerInfo.isSessionLayer) { - ImGui::TextColored(ImVec4(0.5f, 0.7f, 1.0f, 1.0f), "Session"); - } else if (layerInfo.isAnonymous) { - ImGui::Text("Anonymous"); - } else { - ImGui::Text("Sublayer"); - } - - // Context menu - RenderLayerContextMenu(i); - } - - ImGui::EndTable(); - } -} - -void LayerPanel::RenderLayerContextMenu(int layerIndex) { - if (ImGui::BeginPopupContextItem(("layer_ctx_" + std::to_string(layerIndex)).c_str())) { - auto layers = m_layerManager->GetLayerStack(); - if (layerIndex < 0 || layerIndex >= static_cast(layers.size())) { - ImGui::EndPopup(); - return; - } - - const auto& info = layers[layerIndex]; - - if (ImGui::MenuItem(info.isMuted ? "Unmute" : "Mute")) { - if (info.isMuted) { - m_layerManager->UnmuteLayer(info.identifier); - } else { - m_layerManager->MuteLayer(info.identifier); - } - } - - ImGui::Separator(); - - if (!info.isRootLayer && !info.isSessionLayer) { - if (ImGui::MenuItem("Move Up", nullptr, false, layerIndex > 0)) { - if (m_commandHistory) { - // Capture order before move. - auto layers = m_layerManager->GetLayerStack(); - std::vector before, after; - for (auto& l : layers) - if (!l.isRootLayer && !l.isSessionLayer) - before.push_back(l.identifier); - after = before; - // Find index within sublayer-only list. - int subIdx = -1; - for (int i = 0; i < static_cast(before.size()); ++i) - if (before[i] == info.identifier) { subIdx = i; break; } - if (subIdx > 0) std::swap(after[subIdx], after[subIdx - 1]); - m_commandHistory->Push(std::make_unique( - m_layerManager, before, after)); - } else { - m_layerManager->MoveSublayerUp(layerIndex); - } - } - if (ImGui::MenuItem("Move Down", nullptr, false, layerIndex < static_cast(layers.size()) - 1)) { - if (m_commandHistory) { - auto layersNow = m_layerManager->GetLayerStack(); - std::vector before, after; - for (auto& l : layersNow) - if (!l.isRootLayer && !l.isSessionLayer) - before.push_back(l.identifier); - after = before; - int subIdx = -1; - for (int i = 0; i < static_cast(before.size()); ++i) - if (before[i] == info.identifier) { subIdx = i; break; } - if (subIdx >= 0 && subIdx + 1 < static_cast(after.size())) - std::swap(after[subIdx], after[subIdx + 1]); - m_commandHistory->Push(std::make_unique( - m_layerManager, before, after)); - } else { - m_layerManager->MoveSublayerDown(layerIndex); - } - } - - ImGui::Separator(); - - if (ImGui::MenuItem("Remove")) { - if (m_commandHistory) { - m_commandHistory->Push(std::make_unique( - m_layerManager, layerIndex)); - } else { - m_layerManager->RemoveSublayer(layerIndex); - } - } - } - - ImGui::EndPopup(); - } -} - -void LayerPanel::ShowCreateLayerDialog() { - ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always); - ImGui::OpenPopup("Create New Layer"); - - if (ImGui::BeginPopupModal("Create New Layer", &m_showCreateDialog)) { - ImGui::Text("Layer Name:"); - ImGui::InputText("##name", m_newLayerName, sizeof(m_newLayerName)); - - ImGui::Spacing(); - ImGui::Text("Save Path:"); - ImGui::InputText("##path", m_newLayerPath, sizeof(m_newLayerPath)); - ImGui::SameLine(); - if (ImGui::Button("Browse...")) { - // TODO: File save dialog - } - - ImGui::Spacing(); - - if (ImGui::Button("Create", ImVec2(120, 0))) { - std::string path; - if (m_newLayerPath[0] != '\0') { - path = std::string(m_newLayerPath) + "/" + m_newLayerName; - } else { - path = m_newLayerName; - } - - if (!path.empty()) { - if (m_commandHistory) { - m_commandHistory->Push(std::make_unique( - m_layerManager, "./" + path)); - } else { - m_layerManager->CreateSublayer("./" + path); - } - m_showCreateDialog = false; - } - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) { - m_showCreateDialog = false; - } - - ImGui::EndPopup(); - } -} - -} // namespace UsdLayerManager \ No newline at end of file diff --git a/src/ui/LayerPanel.h b/src/ui/LayerPanel.h deleted file mode 100644 index 5a5de61..0000000 --- a/src/ui/LayerPanel.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "../core/LayerManager.h" -#include "../core/CommandHistory.h" -#include -#include -#include -#include - -namespace UsdLayerManager { - -class LayerPanel { -public: - LayerPanel(); - ~LayerPanel(); - - void SetLayerManager(LayerManager* manager); - void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } - void Render(); - -private: - void RenderLayerContextMenu(int layerIndex); - void ShowCreateLayerDialog(); - - LayerManager* m_layerManager; - CommandHistory* m_commandHistory = nullptr; - int m_selectedLayerIndex; - bool m_showCreateDialog; - char m_newLayerPath[256]; - char m_newLayerName[128]; -}; - -} // namespace UsdLayerManager \ No newline at end of file diff --git a/src/ui/StageEditorPanel.cpp b/src/ui/StageEditorPanel.cpp new file mode 100644 index 0000000..8711a33 --- /dev/null +++ b/src/ui/StageEditorPanel.cpp @@ -0,0 +1,392 @@ +#include "StageEditorPanel.h" +#include "../utils/FileDialog.h" +#include "../core/commands/LayerCommands.h" +#include +#include +#include + +namespace UsdLayerManager { + +StageEditorPanel::StageEditorPanel() { + m_newLayerPath[0] = '\0'; + m_newLayerName[0] = '\0'; +} + +StageEditorPanel::~StageEditorPanel() {} + +// --------------------------------------------------------------------------- +void StageEditorPanel::Render() { + if (!m_layerManager) return; + + auto iconBtn = [&](const char* id, Icon icon, const char* fallback) -> bool { + if (m_iconManager) { + ImTextureID tex = m_iconManager->Get(icon); + return ImGui::ImageButton(id, ImTextureRef(tex), ImVec2(18.f, 18.f)); + } + return ImGui::Button(fallback); + }; + + // Toolbar + ImGui::Text("Stage Editor"); + ImGui::SameLine(ImGui::GetWindowWidth() - 100); + + if (iconBtn("##refresh", Icon::Refresh, "R")) + m_layerManager->Refresh(); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Refresh"); + + ImGui::SameLine(); + if (iconBtn("##addExisting", Icon::FolderOpen, "+E")) { + std::string path = FileDialog::OpenFile( + "USD Files (*.usd;*.usda;*.usdc)\0*.usd;*.usda;*.usdc\0All Files (*.*)\0*.*\0", + "Add Sublayer"); + if (!path.empty()) { + if (m_commandHistory) + m_commandHistory->Push( + std::make_unique(m_layerManager, path)); + else + m_layerManager->CreateSublayer(path); + } + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Add Existing Sublayer"); + + ImGui::SameLine(); + if (iconBtn("##createNew", Icon::FilePlus, "+N")) { + m_showCreateDialog = true; + m_newLayerPath[0] = '\0'; + strcpy_s(m_newLayerName, "new_layer.usd"); + } + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Create New Sublayer"); + + ImGui::Separator(); + + auto allLayers = m_layerManager->GetLayerStack(); + if (allLayers.empty()) { + ImGui::TextDisabled("No stage loaded"); + if (m_showCreateDialog) ShowCreateLayerDialog(); + return; + } + + std::vector fixed, sublayers; + for (const auto& li : allLayers) { + if (li.isRootLayer || li.isSessionLayer) + fixed.push_back(li); + else + sublayers.push_back(li); + } + + RenderFixedLayers(fixed); + + ImGui::Spacing(); + ImGui::TextDisabled("Sublayers"); + ImGui::Separator(); + + RenderSublayerList(sublayers); + + if (m_showCreateDialog) ShowCreateLayerDialog(); +} + +// --------------------------------------------------------------------------- +void StageEditorPanel::RenderFixedLayers(const std::vector& layers) { + if (layers.empty()) return; + + if (!ImGui::BeginTable("FixedLayerTable", 4, + ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_SizingFixedFit)) + return; + + ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 70.0f); + ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f); + + for (const auto& li : layers) { + ImGui::TableNextRow(); + ImGui::PushID(li.identifier.c_str()); + + ImGui::TableSetColumnIndex(0); + if (li.isDirty) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*"); + else + ImGui::TextDisabled(" "); + + ImGui::TableSetColumnIndex(1); + ImVec4 color = li.isRootLayer + ? ImVec4(0.5f, 1.0f, 0.5f, 1.0f) + : ImVec4(0.5f, 0.7f, 1.0f, 1.0f); + ImGui::TextColored(color, "%s", li.displayName.c_str()); + if (ImGui::IsItemHovered() && !li.realPath.empty()) + ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str()); + + ImGui::TableSetColumnIndex(2); + if (li.isRootLayer) + ImGui::TextColored(ImVec4(0.5f, 1.0f, 0.5f, 1.0f), "Root"); + else + ImGui::TextColored(ImVec4(0.5f, 0.7f, 1.0f, 1.0f), "Session"); + + ImGui::TableSetColumnIndex(3); + { + bool isET = li.isEditTarget; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + if (ImGui::Checkbox("##et", &isET) && isET) + m_layerManager->SetEditTarget(li.identifier); + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); +} + +// --------------------------------------------------------------------------- +void StageEditorPanel::RenderSublayerList(const std::vector& sublayers) { + if (sublayers.empty()) { + ImGui::TextDisabled(" No sublayers"); + return; + } + + float tableHeight = ImGui::GetContentRegionAvail().y; + + if (!ImGui::BeginTable("SublayerTable", 5, + ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollY, + ImVec2(0, tableHeight))) + return; + + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableSetupColumn("##drag", ImGuiTableColumnFlags_WidthFixed, 14.0f); + ImGui::TableSetupColumn("##dirty", ImGuiTableColumnFlags_WidthFixed, 14.0f); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("##et", ImGuiTableColumnFlags_WidthFixed, 28.0f); + ImGui::TableSetupColumn("Muted", ImGuiTableColumnFlags_WidthFixed, 55.0f); + + // Custom header row: pen icon for the ET column, text for the rest. + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + ImGui::TableSetColumnIndex(2); ImGui::TableHeader("Name"); + ImGui::TableSetColumnIndex(3); + { + float cellW = ImGui::GetContentRegionAvail().x; + float iconW = 14.0f; + float off = (cellW - iconW) * 0.5f; + if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off); + if (m_iconManager) + ImGui::Image(ImTextureRef(m_iconManager->Get(Icon::Pen)), ImVec2(iconW, iconW)); + else + ImGui::TextUnformatted("ET"); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("Edit Target"); + } + ImGui::TableSetColumnIndex(4); ImGui::TableHeader("Muted"); + + for (int i = 0; i < static_cast(sublayers.size()); i++) { + const auto& li = sublayers[i]; + ImGui::TableNextRow(); + ImGui::PushID(i); + + // Col 0: invisible span-all Selectable for row selection / drag / context menu. + ImGui::TableSetColumnIndex(0); + ImVec2 rowStart = ImGui::GetCursorScreenPos(); // save before Selectable moves cursor + + bool isSelected = (m_selectedIdx == i); + if (ImGui::Selectable("##row", isSelected, + ImGuiSelectableFlags_SpanAllColumns | + ImGuiSelectableFlags_AllowOverlap, + ImVec2(0, 0))) + m_selectedIdx = i; + + // Context menu: must come immediately after the span-all Selectable so + // BeginPopupContextItem sees it as the "last item" and responds to + // right-click anywhere in the row. + RenderSublayerContextMenu(i, sublayers); + + // Drag source: also attached to the Selectable (left-button drag). + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID)) { + ImGui::SetDragDropPayload("SUBLAYER_IDX", &i, sizeof(int)); + ImGui::Text("%s", li.displayName.c_str()); + ImGui::EndDragDropSource(); + } + + // Drop target on the Selectable. + if (ImGui::BeginDragDropTarget()) { + if (const ImGuiPayload* payload = + ImGui::AcceptDragDropPayload("SUBLAYER_IDX")) { + int src = *static_cast(payload->Data); + int dst = i; + if (src != dst) { + std::vector before, after; + for (const auto& sl : sublayers) before.push_back(sl.identifier); + after = before; + std::string moved = after[src]; + after.erase(after.begin() + src); + after.insert(after.begin() + dst, moved); + if (m_commandHistory) { + m_commandHistory->Push(std::make_unique( + m_layerManager, before, after)); + } else { + LayerReorderCommand cmd(m_layerManager, before, after); + static_cast(cmd).Execute(); + } + } + } + ImGui::EndDragDropTarget(); + } + + // "=" drag handle: overlay at col 0 using the saved screen position so it + // sits inside the row, not below it (TableSetColumnIndex only resets X). + ImGui::SetCursorScreenPos(rowStart); + ImGui::TextDisabled("="); + + // Col 1: dirty indicator + ImGui::TableSetColumnIndex(1); + if (li.isDirty) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), "*"); + else + ImGui::TextDisabled(" "); + + // Col 2: layer name + ImGui::TableSetColumnIndex(2); + ImVec4 nameColor = li.isMuted + ? ImVec4(0.5f, 0.5f, 0.5f, 1.0f) + : ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + ImGui::TextColored(nameColor, "%s", li.displayName.c_str()); + if (ImGui::IsItemHovered() && !li.realPath.empty()) + ImGui::SetTooltip("%s\n%s", li.identifier.c_str(), li.realPath.c_str()); + + // Col 3: edit target checkbox — checking sets this layer as edit target; + // unchecking does nothing (there is always an active edit target). + ImGui::TableSetColumnIndex(3); + { + bool isET = li.isEditTarget; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + if (ImGui::Checkbox("##et", &isET) && isET) + m_layerManager->SetEditTarget(li.identifier); + ImGui::PopStyleVar(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(li.isEditTarget ? "Edit target (active)" : "Set as edit target"); + } + + // Col 4: mute checkbox + ImGui::TableSetColumnIndex(4); + { + bool muted = li.isMuted; + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); + if (ImGui::Checkbox("##muted", &muted)) { + if (muted) m_layerManager->MuteLayer(li.identifier); + else m_layerManager->UnmuteLayer(li.identifier); + } + ImGui::PopStyleVar(); + } + + ImGui::PopID(); + } + + ImGui::EndTable(); +} + +// --------------------------------------------------------------------------- +void StageEditorPanel::RenderSublayerContextMenu(int i, + const std::vector& sublayers) { + if (!ImGui::BeginPopupContextItem(("ctx_" + std::to_string(i)).c_str())) + return; + + const auto& li = sublayers[i]; + + if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute")) { + if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier); + else m_layerManager->MuteLayer(li.identifier); + } + + if (!li.isEditTarget && ImGui::MenuItem("Set as Edit Target")) + m_layerManager->SetEditTarget(li.identifier); + + ImGui::Separator(); + + bool canUp = (i > 0); + bool canDown = (i < static_cast(sublayers.size()) - 1); + + auto buildBeforeAfter = [&](int a, int b, + std::vector& before, + std::vector& after) { + for (const auto& sl : sublayers) before.push_back(sl.identifier); + after = before; + std::swap(after[a], after[b]); + }; + + if (ImGui::MenuItem("Move Up", nullptr, false, canUp)) { + std::vector before, after; + buildBeforeAfter(i, i - 1, before, after); + if (m_commandHistory) + m_commandHistory->Push( + std::make_unique(m_layerManager, before, after)); + else { + LayerReorderCommand cmd(m_layerManager, before, after); + static_cast(cmd).Execute(); + } + } + + if (ImGui::MenuItem("Move Down", nullptr, false, canDown)) { + std::vector before, after; + buildBeforeAfter(i, i + 1, before, after); + if (m_commandHistory) + m_commandHistory->Push( + std::make_unique(m_layerManager, before, after)); + else { + LayerReorderCommand cmd(m_layerManager, before, after); + static_cast(cmd).Execute(); + } + } + + ImGui::Separator(); + + if (ImGui::MenuItem("Remove")) { + if (m_commandHistory) + m_commandHistory->Push( + std::make_unique(m_layerManager, i)); + else + m_layerManager->RemoveSublayer(i); + } + + ImGui::EndPopup(); +} + +// --------------------------------------------------------------------------- +void StageEditorPanel::ShowCreateLayerDialog() { + ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always); + ImGui::OpenPopup("Create Sublayer"); + + if (ImGui::BeginPopupModal("Create Sublayer", &m_showCreateDialog)) { + ImGui::Text("Layer Name:"); + ImGui::InputText("##name", m_newLayerName, sizeof(m_newLayerName)); + + ImGui::Spacing(); + ImGui::Text("Save Path:"); + ImGui::InputText("##path", m_newLayerPath, sizeof(m_newLayerPath)); + + ImGui::Spacing(); + + if (ImGui::Button("Create", ImVec2(120, 0))) { + std::string path; + if (m_newLayerPath[0] != '\0') + path = std::string(m_newLayerPath) + "/" + m_newLayerName; + else + path = m_newLayerName; + + if (!path.empty()) { + if (m_commandHistory) + m_commandHistory->Push(std::make_unique( + m_layerManager, "./" + path)); + else + m_layerManager->CreateSublayer("./" + path); + m_showCreateDialog = false; + } + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) + m_showCreateDialog = false; + + ImGui::EndPopup(); + } +} + +} // namespace UsdLayerManager diff --git a/src/ui/StageEditorPanel.h b/src/ui/StageEditorPanel.h new file mode 100644 index 0000000..399b586 --- /dev/null +++ b/src/ui/StageEditorPanel.h @@ -0,0 +1,37 @@ +#pragma once + +#include "../core/LayerManager.h" +#include "../core/CommandHistory.h" +#include "IconManager.h" +#include +#include +#include + +namespace UsdLayerManager { + +class StageEditorPanel { +public: + StageEditorPanel(); + ~StageEditorPanel(); + + void SetLayerManager(LayerManager* manager) { m_layerManager = manager; } + void SetCommandHistory(CommandHistory* history) { m_commandHistory = history; } + void SetIconManager(IconManager* icons) { m_iconManager = icons; } + void Render(); + +private: + void RenderFixedLayers(const std::vector& layers); + void RenderSublayerList(const std::vector& sublayers); + void RenderSublayerContextMenu(int i, const std::vector& sublayers); + void ShowCreateLayerDialog(); + + LayerManager* m_layerManager = nullptr; + CommandHistory* m_commandHistory = nullptr; + IconManager* m_iconManager = nullptr; + int m_selectedIdx = -1; + bool m_showCreateDialog = false; + char m_newLayerPath[256]; + char m_newLayerName[128]; +}; + +} // namespace UsdLayerManager