Add collapsible Sublayers section to Scene Hierarchy panel
Puts the sublayer stack directly under the edit-target combo in the Scene Hierarchy, so muting and reordering no longer require switching to the separate Stage Editor window. Each row mirrors StageEditorPanel: drag handle with drag-drop reorder, dirty indicator, name (dimmed when muted, [RL] badge for render-layer sublayers), edit-target checkbox and mute checkbox. Adds dedicated up/down chevron icon buttons for reordering, which the Stage Editor only exposed as context-menu items. Reorder goes through the existing LayerReorderCommand so it is undoable; mute calls LayerManager directly, matching current app-wide behaviour. New Icon::ChevronUp / ChevronDown with matching SVGs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M41.4 233.4c12.5-12.5 32.8-12.5 45.3 0L256 402.7 425.4 233.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0l-192-192c-12.5-12.5-12.5-32.8 0-45.3z"/></svg>
|
||||
|
After Width: | Height: | Size: 471 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>
|
||||
|
After Width: | Height: | Size: 460 B |
@@ -61,6 +61,8 @@ static const char* IconFilename(Icon icon) {
|
||||
case Icon::FilePlus: return "file-plus.svg";
|
||||
case Icon::Pen: return "pen.svg";
|
||||
case Icon::Settings: return "gear.svg";
|
||||
case Icon::ChevronUp: return "chevron-up.svg";
|
||||
case Icon::ChevronDown: return "chevron-down.svg";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
@@ -77,6 +79,7 @@ static constexpr Icon kAllIcons[] = {
|
||||
Icon::SkipBack, Icon::StepBack, Icon::PlayBack, Icon::Play, Icon::Pause,
|
||||
Icon::StepForward, Icon::SkipEnd, Icon::Loop, Icon::Bounce,
|
||||
Icon::Refresh, Icon::FilePlus, Icon::Pen, Icon::Settings,
|
||||
Icon::ChevronUp, Icon::ChevronDown,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -55,6 +55,9 @@ enum class Icon {
|
||||
FilePlus, // create new file
|
||||
Pen, // edit / edit target
|
||||
Settings, // gear / viewport options
|
||||
// List reordering
|
||||
ChevronUp, // move item up
|
||||
ChevronDown, // move item down
|
||||
};
|
||||
|
||||
/// Loads SVG files from disk, rasterizes them with NanoSVG, uploads them as
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../core/commands/RenamePrimCommand.h"
|
||||
#include "../core/commands/ReparentPrimCommand.h"
|
||||
#include "../core/commands/GroupPrimsCommand.h"
|
||||
#include "../core/commands/LayerCommands.h"
|
||||
#include <pxr/usd/usd/prim.h>
|
||||
#include <pxr/usd/usd/primRange.h>
|
||||
#include <pxr/usd/usd/references.h>
|
||||
@@ -309,6 +310,8 @@ void SceneHierarchyPanel::Render() {
|
||||
}
|
||||
}
|
||||
|
||||
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||
ImGui::BeginDisabled(renderLayerActive);
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::BeginCombo("##layerswitch", currentLabel.c_str(),
|
||||
ImGuiComboFlags_HeightRegular)) {
|
||||
@@ -326,9 +329,16 @@ void SceneHierarchyPanel::Render() {
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
bool comboHovered = ImGui::IsItemHovered();
|
||||
ImGui::EndDisabled();
|
||||
if (renderLayerActive && comboHovered)
|
||||
ImGui::SetTooltip("Edit target is locked to the active render layer");
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
RenderSublayerSection();
|
||||
ImGui::Spacing();
|
||||
|
||||
auto paths = m_propertyManager->GetPrimPaths();
|
||||
if (paths.empty()) {
|
||||
ImGui::TextDisabled("No prims in stage");
|
||||
@@ -582,6 +592,272 @@ void SceneHierarchyPanel::Render() {
|
||||
ProcessPendingReplaceRef();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void SceneHierarchyPanel::RenderSublayerSection() {
|
||||
if (!ImGui::CollapsingHeader("Sublayers", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
return;
|
||||
|
||||
if (!m_layerManager) {
|
||||
ImGui::TextDisabled(" No layer manager");
|
||||
return;
|
||||
}
|
||||
|
||||
auto sublayers = m_layerManager->GetSublayers();
|
||||
if (sublayers.empty()) {
|
||||
ImGui::TextDisabled(" No sublayers");
|
||||
return;
|
||||
}
|
||||
|
||||
const float rowHeight = ImGui::GetTextLineHeightWithSpacing();
|
||||
const int visibleRows = std::min<int>(static_cast<int>(sublayers.size()), 6);
|
||||
float tableHeight = rowHeight * (visibleRows + 1); // +1 for header row
|
||||
|
||||
if (!ImGui::BeginTable("HierarchySublayerTable", 6,
|
||||
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);
|
||||
ImGui::TableSetupColumn("##order", ImGuiTableColumnFlags_WidthFixed, 44.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");
|
||||
ImGui::TableSetColumnIndex(5); ImGui::TableHeader("Order");
|
||||
|
||||
for (int i = 0; i < static_cast<int>(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_sublayerSelectedIdx == i);
|
||||
if (ImGui::Selectable("##row", isSelected,
|
||||
ImGuiSelectableFlags_SpanAllColumns |
|
||||
ImGuiSelectableFlags_AllowOverlap,
|
||||
ImVec2(0, 0)))
|
||||
m_sublayerSelectedIdx = 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("HIER_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("HIER_SUBLAYER_IDX")) {
|
||||
int src = *static_cast<const int*>(payload->Data);
|
||||
int dst = i;
|
||||
if (src != dst) {
|
||||
std::vector<std::string> 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<LayerReorderCommand>(
|
||||
m_layerManager, before, after));
|
||||
} else {
|
||||
LayerReorderCommand cmd(m_layerManager, before, after);
|
||||
static_cast<ICommand&>(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(" ");
|
||||
|
||||
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
|
||||
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||
|
||||
// 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());
|
||||
if (isRenderLayer) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.2f, 1.0f), "[RL]");
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||
}
|
||||
|
||||
// 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));
|
||||
ImGui::BeginDisabled(renderLayerActive);
|
||||
if (ImGui::Checkbox("##et", &isET) && isET)
|
||||
m_layerManager->SetEditTarget(li.identifier);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(renderLayerActive ? "Edit target is locked to the active render layer"
|
||||
: li.isEditTarget ? "Edit target (active)"
|
||||
: "Set as edit target");
|
||||
}
|
||||
|
||||
// Col 4: mute checkbox — disabled for render-layer sublayers, which
|
||||
// RenderLayerManager mutes/unmutes exclusively as layers are switched.
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
{
|
||||
bool muted = li.isMuted;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f));
|
||||
ImGui::BeginDisabled(isRenderLayer);
|
||||
if (ImGui::Checkbox("##muted", &muted)) {
|
||||
if (muted) m_layerManager->MuteLayer(li.identifier);
|
||||
else m_layerManager->UnmuteLayer(li.identifier);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleVar();
|
||||
if (isRenderLayer && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||
}
|
||||
|
||||
// Col 5: up/down reorder icon buttons.
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
{
|
||||
bool canUp = (i > 0);
|
||||
bool canDown = (i < static_cast<int>(sublayers.size()) - 1);
|
||||
|
||||
auto buildBeforeAfter = [&](int a, int b,
|
||||
std::vector<std::string>& before,
|
||||
std::vector<std::string>& after) {
|
||||
for (const auto& sl : sublayers) before.push_back(sl.identifier);
|
||||
after = before;
|
||||
std::swap(after[a], after[b]);
|
||||
};
|
||||
|
||||
ImVec2 iconSize(14.0f, 14.0f);
|
||||
|
||||
ImGui::BeginDisabled(!canUp);
|
||||
ImTextureID upTex = m_iconManager ? m_iconManager->Get(Icon::ChevronUp) : ImTextureID_Invalid;
|
||||
if (m_iconManager ? ImGui::ImageButton("##up", ImTextureRef(upTex), iconSize)
|
||||
: ImGui::ArrowButton("##up", ImGuiDir_Up)) {
|
||||
std::vector<std::string> before, after;
|
||||
buildBeforeAfter(i, i - 1, before, after);
|
||||
if (m_commandHistory)
|
||||
m_commandHistory->Push(
|
||||
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
|
||||
else {
|
||||
LayerReorderCommand cmd(m_layerManager, before, after);
|
||||
static_cast<ICommand&>(cmd).Execute();
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Up");
|
||||
|
||||
ImGui::SameLine(0.0f, 2.0f);
|
||||
|
||||
ImGui::BeginDisabled(!canDown);
|
||||
ImTextureID downTex = m_iconManager ? m_iconManager->Get(Icon::ChevronDown) : ImTextureID_Invalid;
|
||||
if (m_iconManager ? ImGui::ImageButton("##down", ImTextureRef(downTex), iconSize)
|
||||
: ImGui::ArrowButton("##down", ImGuiDir_Down)) {
|
||||
std::vector<std::string> before, after;
|
||||
buildBeforeAfter(i, i + 1, before, after);
|
||||
if (m_commandHistory)
|
||||
m_commandHistory->Push(
|
||||
std::make_unique<LayerReorderCommand>(m_layerManager, before, after));
|
||||
else {
|
||||
LayerReorderCommand cmd(m_layerManager, before, after);
|
||||
static_cast<ICommand&>(cmd).Execute();
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move Down");
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void SceneHierarchyPanel::RenderSublayerContextMenu(int i,
|
||||
const std::vector<LayerInfo>& sublayers) {
|
||||
if (!ImGui::BeginPopupContextItem(("ctx_sublayer_" + std::to_string(i)).c_str()))
|
||||
return;
|
||||
|
||||
const auto& li = sublayers[i];
|
||||
bool isRenderLayer = RenderLayerManager::IsRenderLayerSublayer(li.layer);
|
||||
bool renderLayerActive = m_renderLayerManager && !m_renderLayerManager->IsDefaultActive();
|
||||
|
||||
if (ImGui::MenuItem(li.isMuted ? "Unmute" : "Mute", nullptr, false, !isRenderLayer)) {
|
||||
if (li.isMuted) m_layerManager->UnmuteLayer(li.identifier);
|
||||
else m_layerManager->MuteLayer(li.identifier);
|
||||
}
|
||||
if (isRenderLayer && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||
|
||||
if (!li.isEditTarget &&
|
||||
ImGui::MenuItem("Set as Edit Target", nullptr, false, !renderLayerActive))
|
||||
m_layerManager->SetEditTarget(li.identifier);
|
||||
if (renderLayerActive && !li.isEditTarget && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Edit target is locked to the active render layer");
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem("Remove", nullptr, false, !isRenderLayer)) {
|
||||
if (m_commandHistory)
|
||||
m_commandHistory->Push(
|
||||
std::make_unique<LayerRemoveCommand>(m_layerManager, i));
|
||||
else
|
||||
m_layerManager->RemoveSublayer(i);
|
||||
}
|
||||
if (isRenderLayer && ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip("Managed from the Render Layer panel");
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void SceneHierarchyPanel::RenderPrimNode(const UsdPrim& prim) {
|
||||
if (!prim.IsValid()) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user