spatial hierarchy: storey elevation + Long Name column + resizable layout

- helpers/placement: port get_storey_elevation (placement Z, falling back
  to the Elevation attribute), matching ifcopenshell.util.placement.
- Add a secondary column: the storey elevation for IfcBuildingStorey,
  otherwise the LongName when filled. Elevations are right-aligned.
- Columns: Name is drag-resizable (interactive) and defaults to 20% of the
  width, Long Name stretches to fill the rest, and the eye is pinned to the
  right at a fixed width. Header shown so the divider can be grabbed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 20:33:29 +10:00
parent 90196dd51d
commit 4afb3892a2
6 changed files with 95 additions and 11 deletions
@@ -25,6 +25,7 @@
#include <QHeaderView>
#include <QMenu>
#include <QShowEvent>
#include <QSizePolicy>
#include <QTreeWidget>
#include <QTreeWidgetItem>
@@ -50,21 +51,26 @@ SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
tree_ = new QTreeWidget(section);
tree_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
tree_->setColumnCount(2);
tree_->setHeaderLabels({"Spatial Item", ""});
tree_->setColumnCount(3);
tree_->setHeaderLabels({"Name", "Long Name", ""});
tree_->setIconSize(QSize(16, 16));
tree_->setSelectionMode(QAbstractItemView::ExtendedSelection);
tree_->setUniformRowHeights(true);
// Name is drag-resizable, Long Name fills the rest, the eye is pinned to
// the right at a fixed width. Header stays visible so the Name/Long-Name
// divider can be dragged. Initial 20/80 split applied in showEvent once the
// real width is known.
tree_->header()->setStretchLastSection(false);
tree_->header()->setSectionResizeMode(0, QHeaderView::Stretch);
tree_->header()->setSectionResizeMode(1, QHeaderView::Fixed);
tree_->header()->resizeSection(1, 28);
tree_->header()->hide();
tree_->header()->setSectionsMovable(false);
tree_->header()->setSectionResizeMode(0, QHeaderView::Interactive); // name
tree_->header()->setSectionResizeMode(1, QHeaderView::Stretch); // LongName / elevation
tree_->header()->setSectionResizeMode(2, QHeaderView::Fixed); // visibility
tree_->header()->resizeSection(2, 28);
section->addBodyWidget(tree_);
addBodyWidget(section);
connect(tree_, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item, int column) {
if (!item || column != 1) return;
if (!item || column != 2) return;
emit visibilityToggleRequested(itemPath(item));
});
@@ -83,6 +89,20 @@ SpatialHierarchyPanel::SpatialHierarchyPanel(QWidget* parent)
});
}
void SpatialHierarchyPanel::showEvent(QShowEvent* event) {
components::Panel::showEvent(event);
// Default the Name column to 20% of the width once the panel has a real
// layout size; Long Name (stretch) takes the rest. Left interactive after,
// so the user's own drag persists.
if (!column_widths_initialized_) {
const int available = tree_->viewport()->width();
if (available > 100) {
tree_->header()->resizeSection(0, available / 5);
column_widths_initialized_ = true;
}
}
}
void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
tree_->clear();
for (const auto& node : nodes) {
@@ -92,11 +112,15 @@ void SpatialHierarchyPanel::setNodes(const QList<TreeNode>& nodes) {
}
void SpatialHierarchyPanel::addNode(QTreeWidgetItem* parent, const TreeNode& node) {
auto* item = new QTreeWidgetItem(parent, {node.name, ""});
item->setData(1, Qt::UserRole, node.visible);
auto* item = new QTreeWidgetItem(parent, {node.name, node.detail, ""});
item->setData(2, Qt::UserRole, node.visible);
item->setSizeHint(0, QSize(0, 24));
item->setIcon(0, components::icons::makeSvgIcon(iconPath(node.kind)));
item->setIcon(1, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
// Storey elevations read as right-aligned numbers; LongNames stay left.
if (node.kind == ItemKind::Storey) {
item->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
}
item->setIcon(2, components::icons::makeSvgIcon(node.visible ? ":/icons/eye.svg" : ":/icons/eye-closed.svg"));
for (const auto& child : node.children) {
addNode(item, child);
}
@@ -27,6 +27,7 @@
class QTreeWidget;
class QTreeWidgetItem;
class QShowEvent;
namespace bonsaiviewer::modules::spatial_hierarchy {
@@ -40,12 +41,16 @@ public:
signals:
void visibilityToggleRequested(const NodePath& path);
protected:
void showEvent(QShowEvent* event) override;
private:
void addNode(QTreeWidgetItem* parent, const TreeNode& node);
NodePath itemPath(QTreeWidgetItem* item) const;
QString iconPath(ItemKind kind) const;
QTreeWidget* tree_ = nullptr;
bool column_widths_initialized_ = false;
};
} // namespace bonsaiviewer::modules::spatial_hierarchy
@@ -36,6 +36,7 @@ enum class ItemKind {
struct TreeNode {
QString name;
QString detail; // secondary column: LongName, or the elevation for storeys
ItemKind kind = ItemKind::Space;
bool visible = true;
QList<TreeNode> children;
@@ -27,7 +27,8 @@
#include "../../../ifcparse/file.h"
#include "../../../ifcparse/schema.h"
#include "element.h" // helpers: get_spatial_children, get_string_attribute
#include "element.h" // helpers: get_spatial_children, get_string_attribute
#include "placement.h" // helpers: get_storey_elevation
#include <QCollator>
@@ -79,6 +80,13 @@ TreeNode buildNode(const express::Base& element) {
node.name = displayName(element);
node.kind = kindOf(element);
node.visible = true;
// Secondary column: the storey elevation, else the LongName when filled.
if (node.kind == ItemKind::Storey) {
node.detail = QString::number(get_storey_elevation(element));
} else if (auto long_name = get_string_attribute(element, "LongName");
long_name && !long_name->empty()) {
node.detail = QString::fromStdString(*long_name);
}
for (const auto& child : get_spatial_children(element)) {
node.children.append(buildNode(child));
}