From f56f4674ccd8ad7cd53398330a9a38de8e7b8f8b Mon Sep 17 00:00:00 2001 From: indigo Date: Wed, 17 Jun 2026 08:02:41 +0800 Subject: [PATCH] Property panel: token[] view/edit, uniform row heights - Add token[] attribute display: inline InputText (or Combo when allowedTokens metadata is present), collapsible [N tokens] tree for arrays with up to 32 editable entries - Force all property table rows to GetFrameHeight() minimum via TableNextRow min_row_height so text-only and widget rows match - Add AlignTextToFramePadding() before every TextUnformatted / TextDisabled / TextColored in cols 0-2 so short text sits centred in the uniform-height row rather than pinned to the top - Wrap matrix attributes in TreeNodeEx [NxN] so they collapse to a single-height row; relationship targets use inline text / collapsible tree instead of a variable-height ListBox Co-Authored-By: Claude Sonnet 4.6 --- src/ui/PropertyPanel.cpp | 135 ++++++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 22 deletions(-) diff --git a/src/ui/PropertyPanel.cpp b/src/ui/PropertyPanel.cpp index cfd771b..eb4e5ed 100644 --- a/src/ui/PropertyPanel.cpp +++ b/src/ui/PropertyPanel.cpp @@ -550,6 +550,7 @@ static void DrawPropertyMiniButton(const char* label, bool authored, ImGui::PushStyleColor(ImGuiCol_Text, btnColor); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); + ImGui::AlignTextToFramePadding(); ImGui::SmallButton(label); ImGui::PopStyleColor(2); @@ -841,19 +842,28 @@ static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr, return VtValue(); } - // ---- Matrices (multi-row, read-write) ---- - auto drawMatrix = [](const char* id, ImGuiDataType dt, void* data, int rows, int cols) -> bool { + // ---- Matrices: collapsed single row, expand to edit ---- + // Render rows inside a TreeNode so the table row stays one line tall + // when collapsed. Expanded rows are user-initiated so taller is fine. + auto drawMatrix = [](const char* id, ImGuiDataType dt, void* data, + int rows, int cols) -> bool { bool changed = false; - ImGui::PushID(id); - for (int r = 0; r < rows; ++r) { - char rowId[8]; snprintf(rowId, sizeof(rowId), "##r%d", r); - ImGui::InputScalarN(rowId, dt, - static_cast(data) + r * cols * (dt == ImGuiDataType_Double ? 8 : 4), - cols, nullptr, nullptr, - dt == ImGuiDataType_Double ? "%.4f" : "%.4f"); - changed |= ImGui::IsItemDeactivatedAfterEdit(); + char hdr[24]; snprintf(hdr, sizeof(hdr), "[%dx%d]##%s", rows, cols, id); + bool open = ImGui::TreeNodeEx(hdr, + ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth); + if (open) { + ImGui::PushID(id); + for (int r = 0; r < rows; ++r) { + char rowId[8]; snprintf(rowId, sizeof(rowId), "##r%d", r); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputScalarN(rowId, dt, + static_cast(data) + r * cols * (dt == ImGuiDataType_Double ? 8 : 4), + cols, nullptr, nullptr, "%.4f"); + changed |= ImGui::IsItemDeactivatedAfterEdit(); + } + ImGui::PopID(); + ImGui::TreePop(); } - ImGui::PopID(); return changed; }; @@ -890,29 +900,92 @@ static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr, // ---- Token ---- if (val.IsHolding()) { + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(val.UncheckedGet().GetText()); return VtValue(); } // ---- String / AssetPath ---- if (val.IsHolding()) { + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(val.UncheckedGet().c_str()); return VtValue(); } if (val.IsHolding()) { + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(val.UncheckedGet().GetAssetPath().c_str()); return VtValue(); } - // ---- Token array (editable inline) ---- + // ---- Token array: view + edit ---- if (val.IsHolding>()) { - const auto& arr = val.UncheckedGet>(); - ImGui::TextDisabled("[%zu tokens]", arr.size()); + VtArray arr = val.UncheckedGet>(); + bool changed = false; + + // Optional allowedTokens → show Combo per element instead of InputText. + VtValue allowedMeta; + attr.GetMetadata(TfToken("allowedTokens"), &allowedMeta); + const VtArray* allowed = + (!allowedMeta.IsEmpty() && allowedMeta.IsHolding>()) + ? &allowedMeta.UncheckedGet>() + : nullptr; + + // Widget for one element (combo or free-text). + auto drawOne = [&](int idx) { + ImGui::PushID(idx); + if (allowed) { + std::string cur = arr[idx].GetString(); + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::BeginCombo("##tc", cur.c_str())) { + for (const auto& tok : *allowed) { + bool sel = (tok == arr[idx]); + if (ImGui::Selectable(tok.GetText(), sel)) + { arr[idx] = tok; changed = true; } + if (sel) ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "%s", arr[idx].GetText()); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputText("##te", buf, sizeof(buf)); + if (ImGui::IsItemDeactivatedAfterEdit()) + { arr[idx] = TfToken(buf); changed = true; } + } + ImGui::PopID(); + }; + + if (arr.empty()) { + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("[empty]"); + } else if (arr.size() == 1) { + drawOne(0); + } else { + bool open = ImGui::TreeNodeEx("##tarr", + ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth); + ImGui::SameLine(0, 6); + ImGui::TextDisabled("[%zu tokens]", arr.size()); + if (open) { + static constexpr size_t kLimit = 32; + size_t lim = std::min(arr.size(), kLimit); + for (size_t i = 0; i < lim; ++i) { + ImGui::Text("[%d]", (int)i); + ImGui::SameLine(); + drawOne((int)i); + } + if (arr.size() > kLimit) + ImGui::TextDisabled(" ... %zu more", arr.size() - kLimit); + ImGui::TreePop(); + } + } + if (changed) return VtValue(arr); return VtValue(); } // ---- Large arrays: just show size ---- if (val.IsArrayValued() && val.GetArraySize() > 5) { + ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("[array %zu]", val.GetArraySize()); return VtValue(); } @@ -923,6 +996,7 @@ static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr, oss << val; std::string s = oss.str(); if (s.size() > 120) { s.resize(120); s += " ..."; } + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(s.c_str()); } return VtValue(); @@ -1056,11 +1130,12 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { // No TableHeadersRow – matches usdtweak "no header" intent const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget(); + const float rowH = ImGui::GetFrameHeight(); int uid = 0; // ---- Attributes (mirrors usdtweak attribute loop) ---- for (auto& attr : prim.GetAttributes()) { - ImGui::TableNextRow(); + ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); // Col 0 – mini button "(a)" ImGui::TableSetColumnIndex(0); @@ -1073,6 +1148,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { // Col 1 – display name (namespace:basename) ImGui::TableSetColumnIndex(1); + ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(GetPropDisplayName(attr).c_str()); // Col 2 – value widget with PushItemWidth(-FLT_MIN) (label hidden) @@ -1109,9 +1185,12 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { } else if (attr.HasAuthoredConnections()) { SdfPathVector conns; attr.GetConnections(&conns); - if (!conns.empty()) + if (!conns.empty()) { + ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("-> %s", conns[0].GetString().c_str()); + } } else { + ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("no value"); } @@ -1121,7 +1200,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { // ---- Relationships (mirrors usdtweak relationship loop) ---- for (const auto& rel : prim.GetRelationships()) { - ImGui::TableNextRow(); + ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH); // Col 0 – mini button "(r)" ImGui::TableSetColumnIndex(0); @@ -1136,24 +1215,36 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) { ImVec4 relColor = rel.IsAuthored() ? ImVec4(0.6f, 0.9f, 1.f, 1.f) : ImVec4(0.5f, 0.5f, 0.5f, 1.f); + ImGui::AlignTextToFramePadding(); ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str()); - // Col 2 – target list (mirrors DrawUsdRelationshipList in usdtweak) + // Col 2 – target list: single row when collapsed, expand for multiple ImGui::TableSetColumnIndex(2); SdfPathVector targets; rel.GetTargets(&targets); if (targets.empty()) { + ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("no targets"); + } else if (targets.size() == 1) { + ImGui::AlignTextToFramePadding(); + ImGui::TextDisabled("%s", targets[0].GetString().c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", targets[0].GetString().c_str()); } else { ImGui::PushID(rel.GetPath().GetString().c_str()); - float listH = static_cast(targets.size()) * 25.f; - if (ImGui::BeginListBox("##rl", ImVec2(-FLT_MIN, listH))) { + bool open = ImGui::TreeNodeEx("##rtarr", + ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth); + ImGui::SameLine(0, 6); + ImGui::TextDisabled("[%zu targets]", targets.size()); + if (open) { for (const auto& path : targets) { ImGui::PushID(path.GetString().c_str()); - ImGui::TextUnformatted(path.GetString().c_str()); + ImGui::TextDisabled("%s", path.GetString().c_str()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", path.GetString().c_str()); ImGui::PopID(); } - ImGui::EndListBox(); + ImGui::TreePop(); } ImGui::PopID(); }