mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-27 18:57:17 +00:00
properties: filter psets/quantities by name, property, or value
The filter field now actually filters. Typing shows only the sets whose name matches, or that contain a matching property name/value — and when it's a property/value match, only the matching rows are kept (neighbouring rows are dropped). Matching is case-insensitive. Set widgets live in a per-section container that's rebuilt from the raw data on each keystroke, so filtering never recreates the filter field (its focus and cursor are preserved). Placeholder reads "No properties/ quantities" with no data, "No matching properties/quantities" when the filter excludes everything. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,8 @@
|
|||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
QWidget* makePropertySetPanel(const bonsaiviewer::modules::properties::PropertySet& property_set, QWidget* parent = nullptr) {
|
QWidget* makePropertySetPanel(const bonsaiviewer::modules::properties::PropertySet& property_set, QWidget* parent = nullptr) {
|
||||||
@@ -50,6 +52,24 @@ QWidget* makePropertySetPanel(const bonsaiviewer::modules::properties::PropertyS
|
|||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clearLayout(QLayout* layout) {
|
||||||
|
if (!layout) return;
|
||||||
|
while (QLayoutItem* item = layout->takeAt(0)) {
|
||||||
|
if (QWidget* w = item->widget()) delete w;
|
||||||
|
delete item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A container for a section's set widgets, laid out like the section body so it
|
||||||
|
// can be swapped/rebuilt in one place without touching the filter field.
|
||||||
|
QWidget* makeSetContainer(QWidget* parent) {
|
||||||
|
auto* container = new QWidget(parent);
|
||||||
|
auto* layout = new QVBoxLayout(container);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->setSpacing(bonsaiviewer::components::style::metrics::padding);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
QWidget* makeAttributeList(const QList<bonsaiviewer::modules::properties::KeyValueRow>& rows, QWidget* parent = nullptr) {
|
QWidget* makeAttributeList(const QList<bonsaiviewer::modules::properties::KeyValueRow>& rows, QWidget* parent = nullptr) {
|
||||||
QList<bonsaiviewer::components::KeyValueTableRow> table_rows;
|
QList<bonsaiviewer::components::KeyValueTableRow> table_rows;
|
||||||
for (const auto& row : rows) {
|
for (const auto& row : rows) {
|
||||||
@@ -136,16 +156,12 @@ PropertiesPanel::PropertiesPanel(QWidget* parent)
|
|||||||
|
|
||||||
void PropertiesPanel::render(const PropertiesPanelState& state) {
|
void PropertiesPanel::render(const PropertiesPanelState& state) {
|
||||||
clearBodyWidgets();
|
clearBodyWidgets();
|
||||||
|
// The previous widgets were just deleted — drop the stale container pointers
|
||||||
QList<QWidget*> property_set_widgets;
|
// before rebuilding so a stray filter pass can't touch them.
|
||||||
for (const auto& property_set : state.property_sets) {
|
property_sets_data_ = state.property_sets;
|
||||||
property_set_widgets.append(makePropertySetPanel(property_set, this));
|
quantity_sets_data_ = state.quantity_sets;
|
||||||
}
|
properties_container_ = nullptr;
|
||||||
|
quantities_container_ = nullptr;
|
||||||
QList<QWidget*> quantity_set_widgets;
|
|
||||||
for (const auto& property_set : state.quantity_sets) {
|
|
||||||
quantity_set_widgets.append(makePropertySetPanel(property_set, this));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* entity_section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
auto* entity_section = new components::Section("", components::SectionHeaderMode::Hidden, this);
|
||||||
entity_section->addBodyWidget(makeEntityBox(state.entity, this));
|
entity_section->addBodyWidget(makeEntityBox(state.entity, this));
|
||||||
@@ -179,12 +195,12 @@ void PropertiesPanel::render(const PropertiesPanelState& state) {
|
|||||||
});
|
});
|
||||||
connect(properties_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) {
|
connect(properties_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||||
properties_filter_text_ = text;
|
properties_filter_text_ = text;
|
||||||
|
rebuildPropertyWidgets();
|
||||||
});
|
});
|
||||||
properties_section->addBodyWidget(properties_filter_wrapper);
|
properties_section->addBodyWidget(properties_filter_wrapper);
|
||||||
for (auto* widget : property_set_widgets) properties_section->addBodyWidget(widget);
|
properties_container_ = makeSetContainer(properties_section);
|
||||||
if (property_set_widgets.isEmpty()) {
|
properties_section->addBodyWidget(properties_container_);
|
||||||
properties_section->addBodyWidget(makeEmptyStateLabel("No properties", this));
|
rebuildPropertyWidgets();
|
||||||
}
|
|
||||||
properties_section->setExpanded(properties_expanded_);
|
properties_section->setExpanded(properties_expanded_);
|
||||||
properties_filter_toggle->setChecked(properties_filter_visible_);
|
properties_filter_toggle->setChecked(properties_filter_visible_);
|
||||||
|
|
||||||
@@ -209,12 +225,12 @@ void PropertiesPanel::render(const PropertiesPanelState& state) {
|
|||||||
});
|
});
|
||||||
connect(quantities_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) {
|
connect(quantities_filter_field, &QLineEdit::textChanged, this, [this](const QString& text) {
|
||||||
quantities_filter_text_ = text;
|
quantities_filter_text_ = text;
|
||||||
|
rebuildQuantityWidgets();
|
||||||
});
|
});
|
||||||
quantities_section->addBodyWidget(quantities_filter_wrapper);
|
quantities_section->addBodyWidget(quantities_filter_wrapper);
|
||||||
for (auto* widget : quantity_set_widgets) quantities_section->addBodyWidget(widget);
|
quantities_container_ = makeSetContainer(quantities_section);
|
||||||
if (quantity_set_widgets.isEmpty()) {
|
quantities_section->addBodyWidget(quantities_container_);
|
||||||
quantities_section->addBodyWidget(makeEmptyStateLabel("No quantities", this));
|
rebuildQuantityWidgets();
|
||||||
}
|
|
||||||
quantities_section->setExpanded(quantities_expanded_);
|
quantities_section->setExpanded(quantities_expanded_);
|
||||||
quantities_filter_toggle->setChecked(quantities_filter_visible_);
|
quantities_filter_toggle->setChecked(quantities_filter_visible_);
|
||||||
|
|
||||||
@@ -246,4 +262,62 @@ void PropertiesPanel::render(const PropertiesPanelState& state) {
|
|||||||
addBodyWidget(quantities_section);
|
addBodyWidget(quantities_section);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Filter one set: keep it if the filter is empty, or its name matches (then all
|
||||||
|
// rows are kept), or some property name/value matches (then only those rows).
|
||||||
|
// Returns nullopt when nothing in the set matches.
|
||||||
|
std::optional<PropertySet> filterSet(const PropertySet& set, const QString& text) {
|
||||||
|
if (text.isEmpty() || set.title.contains(text, Qt::CaseInsensitive)) {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
PropertySet filtered;
|
||||||
|
filtered.title = set.title;
|
||||||
|
for (const auto& row : set.rows) {
|
||||||
|
if (row.key.contains(text, Qt::CaseInsensitive) ||
|
||||||
|
row.value.contains(text, Qt::CaseInsensitive)) {
|
||||||
|
filtered.rows.append(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filtered.rows.isEmpty()) return std::nullopt;
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild a set container's contents from raw data under the current filter,
|
||||||
|
// dropping non-matching rows, with a placeholder when nothing is shown.
|
||||||
|
void rebuildSetContainer(QWidget* container,
|
||||||
|
const QList<PropertySet>& sets,
|
||||||
|
const QString& filter_text,
|
||||||
|
const QString& empty_text,
|
||||||
|
const QString& no_match_text) {
|
||||||
|
if (!container) return;
|
||||||
|
auto* layout = qobject_cast<QVBoxLayout*>(container->layout());
|
||||||
|
if (!layout) return;
|
||||||
|
clearLayout(layout);
|
||||||
|
|
||||||
|
const QString text = filter_text.trimmed();
|
||||||
|
int shown = 0;
|
||||||
|
for (const auto& set : sets) {
|
||||||
|
if (auto filtered = filterSet(set, text)) {
|
||||||
|
layout->addWidget(makePropertySetPanel(*filtered, container));
|
||||||
|
++shown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shown == 0) {
|
||||||
|
layout->addWidget(makeEmptyStateLabel(sets.isEmpty() ? empty_text : no_match_text, container));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void PropertiesPanel::rebuildPropertyWidgets() {
|
||||||
|
rebuildSetContainer(properties_container_, property_sets_data_, properties_filter_text_,
|
||||||
|
"No properties", "No matching properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
void PropertiesPanel::rebuildQuantityWidgets() {
|
||||||
|
rebuildSetContainer(quantities_container_, quantity_sets_data_, quantities_filter_text_,
|
||||||
|
"No quantities", "No matching quantities");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace bonsaiviewer::modules::properties
|
} // namespace bonsaiviewer::modules::properties
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
class QLabel;
|
class QLabel;
|
||||||
class QLineEdit;
|
class QLineEdit;
|
||||||
class QToolButton;
|
class QToolButton;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
namespace bonsaiviewer::modules::properties {
|
namespace bonsaiviewer::modules::properties {
|
||||||
|
|
||||||
@@ -41,6 +42,14 @@ public:
|
|||||||
void render(const PropertiesPanelState& state);
|
void render(const PropertiesPanelState& state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Rebuild the set widgets inside their container, applying the current
|
||||||
|
// (case-insensitive) filter text: a set is shown only if its name or one of
|
||||||
|
// its property names/values matches, and — when it's a property/value match
|
||||||
|
// rather than a set-name match — only the matching rows are kept. Toggles a
|
||||||
|
// "No properties" / "No matching properties" placeholder.
|
||||||
|
void rebuildPropertyWidgets();
|
||||||
|
void rebuildQuantityWidgets();
|
||||||
|
|
||||||
bool attributes_expanded_ = true;
|
bool attributes_expanded_ = true;
|
||||||
bool relationships_expanded_ = true;
|
bool relationships_expanded_ = true;
|
||||||
bool properties_expanded_ = true;
|
bool properties_expanded_ = true;
|
||||||
@@ -49,6 +58,14 @@ private:
|
|||||||
bool quantities_filter_visible_ = false;
|
bool quantities_filter_visible_ = false;
|
||||||
QString properties_filter_text_;
|
QString properties_filter_text_;
|
||||||
QString quantities_filter_text_;
|
QString quantities_filter_text_;
|
||||||
|
|
||||||
|
// Raw data + the container the set widgets live in, so a filter change can
|
||||||
|
// rebuild just the sets without disturbing the filter field. Recreated on
|
||||||
|
// each render(); the container is owned by its section.
|
||||||
|
QList<PropertySet> property_sets_data_;
|
||||||
|
QList<PropertySet> quantity_sets_data_;
|
||||||
|
QWidget* properties_container_ = nullptr;
|
||||||
|
QWidget* quantities_container_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace bonsaiviewer::modules::properties
|
} // namespace bonsaiviewer::modules::properties
|
||||||
|
|||||||
Reference in New Issue
Block a user