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 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 08:02:41 +08:00
parent 251174ddc3
commit f56f4674cc
+113 -22
View File
@@ -550,6 +550,7 @@ static void DrawPropertyMiniButton(const char* label, bool authored,
ImGui::PushStyleColor(ImGuiCol_Text, btnColor); ImGui::PushStyleColor(ImGuiCol_Text, btnColor);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::AlignTextToFramePadding();
ImGui::SmallButton(label); ImGui::SmallButton(label);
ImGui::PopStyleColor(2); ImGui::PopStyleColor(2);
@@ -841,19 +842,28 @@ static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr,
return VtValue(); return VtValue();
} }
// ---- Matrices (multi-row, read-write) ---- // ---- Matrices: collapsed single row, expand to edit ----
auto drawMatrix = [](const char* id, ImGuiDataType dt, void* data, int rows, int cols) -> bool { // 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; bool changed = false;
ImGui::PushID(id); char hdr[24]; snprintf(hdr, sizeof(hdr), "[%dx%d]##%s", rows, cols, id);
for (int r = 0; r < rows; ++r) { bool open = ImGui::TreeNodeEx(hdr,
char rowId[8]; snprintf(rowId, sizeof(rowId), "##r%d", r); ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::InputScalarN(rowId, dt, if (open) {
static_cast<char*>(data) + r * cols * (dt == ImGuiDataType_Double ? 8 : 4), ImGui::PushID(id);
cols, nullptr, nullptr, for (int r = 0; r < rows; ++r) {
dt == ImGuiDataType_Double ? "%.4f" : "%.4f"); char rowId[8]; snprintf(rowId, sizeof(rowId), "##r%d", r);
changed |= ImGui::IsItemDeactivatedAfterEdit(); ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputScalarN(rowId, dt,
static_cast<char*>(data) + r * cols * (dt == ImGuiDataType_Double ? 8 : 4),
cols, nullptr, nullptr, "%.4f");
changed |= ImGui::IsItemDeactivatedAfterEdit();
}
ImGui::PopID();
ImGui::TreePop();
} }
ImGui::PopID();
return changed; return changed;
}; };
@@ -890,29 +900,92 @@ static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr,
// ---- Token ---- // ---- Token ----
if (val.IsHolding<TfToken>()) { if (val.IsHolding<TfToken>()) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(val.UncheckedGet<TfToken>().GetText()); ImGui::TextUnformatted(val.UncheckedGet<TfToken>().GetText());
return VtValue(); return VtValue();
} }
// ---- String / AssetPath ---- // ---- String / AssetPath ----
if (val.IsHolding<std::string>()) { if (val.IsHolding<std::string>()) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(val.UncheckedGet<std::string>().c_str()); ImGui::TextUnformatted(val.UncheckedGet<std::string>().c_str());
return VtValue(); return VtValue();
} }
if (val.IsHolding<SdfAssetPath>()) { if (val.IsHolding<SdfAssetPath>()) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(val.UncheckedGet<SdfAssetPath>().GetAssetPath().c_str()); ImGui::TextUnformatted(val.UncheckedGet<SdfAssetPath>().GetAssetPath().c_str());
return VtValue(); return VtValue();
} }
// ---- Token array (editable inline) ---- // ---- Token array: view + edit ----
if (val.IsHolding<VtArray<TfToken>>()) { if (val.IsHolding<VtArray<TfToken>>()) {
const auto& arr = val.UncheckedGet<VtArray<TfToken>>(); VtArray<TfToken> arr = val.UncheckedGet<VtArray<TfToken>>();
ImGui::TextDisabled("[%zu tokens]", arr.size()); bool changed = false;
// Optional allowedTokens → show Combo per element instead of InputText.
VtValue allowedMeta;
attr.GetMetadata(TfToken("allowedTokens"), &allowedMeta);
const VtArray<TfToken>* allowed =
(!allowedMeta.IsEmpty() && allowedMeta.IsHolding<VtArray<TfToken>>())
? &allowedMeta.UncheckedGet<VtArray<TfToken>>()
: 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(); return VtValue();
} }
// ---- Large arrays: just show size ---- // ---- Large arrays: just show size ----
if (val.IsArrayValued() && val.GetArraySize() > 5) { if (val.IsArrayValued() && val.GetArraySize() > 5) {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("[array %zu]", val.GetArraySize()); ImGui::TextDisabled("[array %zu]", val.GetArraySize());
return VtValue(); return VtValue();
} }
@@ -923,6 +996,7 @@ static pxr::VtValue DrawVtValueWidget(pxr::UsdAttribute& attr,
oss << val; oss << val;
std::string s = oss.str(); std::string s = oss.str();
if (s.size() > 120) { s.resize(120); s += " ..."; } if (s.size() > 120) { s.resize(120); s += " ..."; }
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(s.c_str()); ImGui::TextUnformatted(s.c_str());
} }
return VtValue(); return VtValue();
@@ -1056,11 +1130,12 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
// No TableHeadersRow matches usdtweak "no header" intent // No TableHeadersRow matches usdtweak "no header" intent
const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget(); const pxr::UsdEditTarget& editTarget = prim.GetStage()->GetEditTarget();
const float rowH = ImGui::GetFrameHeight();
int uid = 0; int uid = 0;
// ---- Attributes (mirrors usdtweak attribute loop) ---- // ---- Attributes (mirrors usdtweak attribute loop) ----
for (auto& attr : prim.GetAttributes()) { for (auto& attr : prim.GetAttributes()) {
ImGui::TableNextRow(); ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
// Col 0 mini button "(a)" // Col 0 mini button "(a)"
ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(0);
@@ -1073,6 +1148,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
// Col 1 display name (namespace:basename) // Col 1 display name (namespace:basename)
ImGui::TableSetColumnIndex(1); ImGui::TableSetColumnIndex(1);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(GetPropDisplayName(attr).c_str()); ImGui::TextUnformatted(GetPropDisplayName(attr).c_str());
// Col 2 value widget with PushItemWidth(-FLT_MIN) (label hidden) // 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()) { } else if (attr.HasAuthoredConnections()) {
SdfPathVector conns; SdfPathVector conns;
attr.GetConnections(&conns); attr.GetConnections(&conns);
if (!conns.empty()) if (!conns.empty()) {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("-> %s", conns[0].GetString().c_str()); ImGui::TextDisabled("-> %s", conns[0].GetString().c_str());
}
} else { } else {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("no value"); ImGui::TextDisabled("no value");
} }
@@ -1121,7 +1200,7 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
// ---- Relationships (mirrors usdtweak relationship loop) ---- // ---- Relationships (mirrors usdtweak relationship loop) ----
for (const auto& rel : prim.GetRelationships()) { for (const auto& rel : prim.GetRelationships()) {
ImGui::TableNextRow(); ImGui::TableNextRow(ImGuiTableRowFlags_None, rowH);
// Col 0 mini button "(r)" // Col 0 mini button "(r)"
ImGui::TableSetColumnIndex(0); ImGui::TableSetColumnIndex(0);
@@ -1136,24 +1215,36 @@ void PropertyPanel::RenderPropertiesTable(const pxr::UsdPrim& prim) {
ImVec4 relColor = rel.IsAuthored() ImVec4 relColor = rel.IsAuthored()
? ImVec4(0.6f, 0.9f, 1.f, 1.f) ? ImVec4(0.6f, 0.9f, 1.f, 1.f)
: ImVec4(0.5f, 0.5f, 0.5f, 1.f); : ImVec4(0.5f, 0.5f, 0.5f, 1.f);
ImGui::AlignTextToFramePadding();
ImGui::TextColored(relColor, "%s", GetPropDisplayName(rel).c_str()); 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); ImGui::TableSetColumnIndex(2);
SdfPathVector targets; SdfPathVector targets;
rel.GetTargets(&targets); rel.GetTargets(&targets);
if (targets.empty()) { if (targets.empty()) {
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("no targets"); 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 { } else {
ImGui::PushID(rel.GetPath().GetString().c_str()); ImGui::PushID(rel.GetPath().GetString().c_str());
float listH = static_cast<float>(targets.size()) * 25.f; bool open = ImGui::TreeNodeEx("##rtarr",
if (ImGui::BeginListBox("##rl", ImVec2(-FLT_MIN, listH))) { ImGuiTreeNodeFlags_AllowOverlap | ImGuiTreeNodeFlags_SpanAvailWidth);
ImGui::SameLine(0, 6);
ImGui::TextDisabled("[%zu targets]", targets.size());
if (open) {
for (const auto& path : targets) { for (const auto& path : targets) {
ImGui::PushID(path.GetString().c_str()); 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::PopID();
} }
ImGui::EndListBox(); ImGui::TreePop();
} }
ImGui::PopID(); ImGui::PopID();
} }