properties: real empty states + smaller base UI font

- Replace the mock IfcWall placeholder with a "No item selected" empty
  state; the panel only fills in class/attributes/relationships/psets from
  a resolved object, and safely stays empty otherwise.
- Show "No properties" / "No quantities" placeholders (muted, themed via
  secondary_text) when those sets are empty.
- Drop the base application font 10pt -> 9pt to fit more data. Panel titles
  keep their own explicit size and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-08 20:37:17 +10:00
parent 4afb3892a2
commit 392af501d1
4 changed files with 27 additions and 54 deletions
+4
View File
@@ -143,6 +143,10 @@ QString buildAppStyleSheet() {
font-size: ${font_small}px;
font-weight: 600;
}
QLabel#panelSectionEmptyLabel {
color: ${secondary_text};
padding: 4px 10px;
}
QToolButton#panelTitleButton {
border: none;
background: transparent;
+4 -1
View File
@@ -41,7 +41,10 @@ void installUiFont() {
}
}
if (!family.isEmpty()) {
QApplication::setFont(QFont(family, 10));
// Slightly smaller base font to fit more data. Panel titles keep their
// own explicit size (QLabel#panelTitleText in Style.cpp), so they're
// unaffected by this.
QApplication::setFont(QFont(family, 9));
}
}
@@ -71,6 +71,12 @@ QWidget* makeRelationshipList(const QList<bonsaiviewer::modules::properties::Rel
return new bonsaiviewer::components::KeyValueTable(table_rows, parent);
}
QLabel* makeEmptyStateLabel(const QString& text, QWidget* parent = nullptr) {
auto* label = new QLabel(text, parent);
label->setObjectName("panelSectionEmptyLabel");
return label;
}
QWidget* makeFilterWrapper(QLineEdit** field_out, QWidget* parent = nullptr) {
auto* wrapper = new QWidget(parent);
wrapper->setObjectName("panelSectionFilterWrapper");
@@ -176,6 +182,9 @@ void PropertiesPanel::render(const PropertiesPanelState& state) {
});
properties_section->addBodyWidget(properties_filter_wrapper);
for (auto* widget : property_set_widgets) properties_section->addBodyWidget(widget);
if (property_set_widgets.isEmpty()) {
properties_section->addBodyWidget(makeEmptyStateLabel("No properties", this));
}
properties_section->setExpanded(properties_expanded_);
properties_filter_toggle->setChecked(properties_filter_visible_);
@@ -203,6 +212,9 @@ void PropertiesPanel::render(const PropertiesPanelState& state) {
});
quantities_section->addBodyWidget(quantities_filter_wrapper);
for (auto* widget : quantity_set_widgets) quantities_section->addBodyWidget(widget);
if (quantity_set_widgets.isEmpty()) {
quantities_section->addBodyWidget(makeEmptyStateLabel("No quantities", this));
}
quantities_section->setExpanded(quantities_expanded_);
quantities_filter_toggle->setChecked(quantities_filter_visible_);
+7 -53
View File
@@ -102,60 +102,14 @@ PropertiesPanelView::PropertiesPanelView(PropertiesPanel* widget,
void PropertiesPanelView::refresh(uint32_t object_id) {
auto* registry = session_state_->elementRegistry();
// Empty default: nothing selected → "No item selected" with empty sections.
// Real data is filled in below when an object resolves.
PropertiesPanelState state;
state.entity = {"IfcWall", "SOLIDWALL"};
state.attributes = {
{"GlobalId", "2Q$n5SLPP9Q8B7wQKjKfUQ"},
{"Name", "Core-EXT-204"},
{"Description", "External load-bearing wall"},
};
state.relationships = {
{"Type", "Basic Wall: Exterior - 200mm"},
{"Container", "Level 02"},
};
state.property_sets = {
{"Pset_WallCommon",
{{"Reference", "Core-EXT-204"},
{"Status", "Reviewed"},
{"Fire Rating", "120 min"},
{"LoadBearing", "True"}}},
{"Identity Data",
{{"Type", "IfcWall"},
{"Name", "Core-EXT-204"},
{"Owner", "Architecture"},
{"Phase", "Construction"}}},
{"BIM Collaboration",
{{"Issue Count", "2 open"},
{"Last Review", "2026-04-30"},
{"Assigned To", "Design Coordination"}}},
};
state.quantity_sets = {
{"BaseQuantities",
{{"Length", "6.20 m"},
{"Height", "3.45 m"},
{"Width", "0.30 m"},
{"Volume", "6.42 m3"}}},
{"Finish Quantities",
{{"NetSideArea", "21.39 m2"},
{"GrossArea", "22.10 m2"},
{"Paint Coverage", "42.78 m2"}}},
};
state.entity = {"No item selected", ""};
if (!registry) {
widget_->render(state);
return;
}
// A real project is loaded — don't leak the placeholder attributes /
// relationships / predefined type. They're populated below from live IFC
// data, or from the cached basics for geometry-only elements.
state.entity.predefined_type.clear();
state.attributes.clear();
state.relationships.clear();
state.property_sets.clear();
state.quantity_sets.clear();
auto entity = registry->findEntity(object_id);
auto entity = registry ? registry->findEntity(object_id)
: std::optional<express::Base>{};
if (entity) {
state.entity.entity_class = QString::fromStdString(entity->declaration().name());
if (auto predefined_type = get_predefined_type(*entity)) {
@@ -183,7 +137,7 @@ void PropertiesPanelView::refresh(uint32_t object_id) {
// occurrence values inheriting from the type.
state.property_sets = toPropertySets(get_psets(*entity, /*psets_only=*/true, /*qtos_only=*/false));
state.quantity_sets = toPropertySets(get_psets(*entity, /*psets_only=*/false, /*qtos_only=*/true));
} else {
} else if (registry) {
// No live IFC source for this object — typical when a pure-geometry
// .ifcview sidecar was loaded without its .ifc/.rdb sibling. Fall back
// to the basic info cached in the element registry so the panel still