feat: implement type-safe ReferenceProperty for SmartPointer fields and add UI support for object selection via context-aware dropdowns

This commit is contained in:
AndreaRigoni
2026-04-17 13:20:21 +00:00
parent ec2d437819
commit 506b8f037f
12 changed files with 265 additions and 12 deletions

View File

@@ -90,6 +90,10 @@ void ContextPanel::setContext(uLib::ObjectsContext* context) {
m_treeView->expandAll();
}
void ContextPanel::setPropertyContext(uLib::ObjectsContext* context) {
m_propertiesPanel->setContext(context);
}
void ContextPanel::onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
uLib::Object* target = nullptr;
if (!selected.indexes().isEmpty()) {

View File

@@ -20,6 +20,7 @@ public:
~ContextPanel();
void setContext(uLib::ObjectsContext* context);
void setPropertyContext(uLib::ObjectsContext* context);
void selectObject(uLib::Object* obj);
void clearSelection();

View File

@@ -128,6 +128,9 @@ void MainPanel::setContext(uLib::ObjectsContext* context) {
m_context = context;
m_contextPanel->setContext(context);
// Propagate context to all panels for reference property dropdowns
m_contextPanel->setPropertyContext(context);
m_firstPane->setContext(context);
if (m_mainVtkContext) {
if (auto* viewport = qobject_cast<uLib::Vtk::QViewport*>(m_firstPane->currentViewport())) {
viewport->RemoveProp3D(*m_mainVtkContext);

View File

@@ -47,4 +47,8 @@ void PropertiesPanel::setObject(uLib::Object* obj) {
m_editor->setObject(obj);
}
void PropertiesPanel::setContext(uLib::ObjectsContext* context) {
m_editor->setContext(context);
}
PropertiesPanel::~PropertiesPanel() {}

View File

@@ -5,6 +5,7 @@
namespace uLib {
class Object;
class ObjectsContext;
namespace Qt { class PropertyEditor; }
}
@@ -24,6 +25,9 @@ public:
/** @brief Sets the object to be inspected. */
void setObject(uLib::Object* obj);
/** @brief Sets the context for reference property dropdowns. */
void setContext(uLib::ObjectsContext* context);
signals:
void propertyUpdated();

View File

@@ -13,6 +13,7 @@
#include <QSlider>
#include <QFontDialog>
#include "Settings.h"
#include "Core/ObjectsContext.h"
namespace uLib {
namespace Qt {
@@ -386,7 +387,74 @@ public:
}
};
PropertyEditor::PropertyEditor(QWidget* parent) : QWidget(parent), m_Object(nullptr) {
////////////////////////////////////////////////////////////////////////////////
// ReferencePropertyWidget
ReferencePropertyWidget::ReferencePropertyWidget(ReferencePropertyBase* prop, ::uLib::ObjectsContext* context, QWidget* parent)
: PropertyWidgetBase(prop, parent), m_RefProp(prop), m_Context(context) {
m_Combo = new QComboBox(static_cast<QWidget*>(this));
m_Layout->addWidget(m_Combo, 1);
refreshCombo();
connect(m_Combo, &QComboBox::currentIndexChanged, this, &ReferencePropertyWidget::onComboChanged);
// Listen for property updates to refresh selected item
m_Connection = uLib::Object::connect(prop, &uLib::Object::Updated, [this](){
QSignalBlocker blocker(m_Combo);
refreshCombo();
});
// Listen for context changes to refresh the dropdown list
if (m_Context) {
m_ContextConnection = uLib::Object::connect(m_Context, &uLib::Object::Updated, [this](){
QSignalBlocker blocker(m_Combo);
refreshCombo();
});
}
}
ReferencePropertyWidget::~ReferencePropertyWidget() {
m_Connection.disconnect();
m_ContextConnection.disconnect();
}
void ReferencePropertyWidget::refreshCombo() {
m_Combo->clear();
m_Combo->addItem("(none)", QVariant::fromValue((quintptr)0));
int selectedIdx = 0;
Object* currentRef = m_RefProp->GetReferencedObject();
if (m_Context) {
const auto& objects = m_Context->GetObjects();
for (auto* obj : objects) {
if (m_RefProp->IsCompatible(obj)) {
QString label = QString::fromStdString(obj->GetInstanceName());
if (label.isEmpty()) {
label = QString::fromStdString(std::string(obj->GetClassName()));
}
// Add index suffix if name is empty to disambiguate
m_Combo->addItem(label, QVariant::fromValue((quintptr)obj));
if (obj == currentRef) {
selectedIdx = m_Combo->count() - 1;
}
}
}
}
m_Combo->setCurrentIndex(selectedIdx);
}
void ReferencePropertyWidget::onComboChanged(int index) {
if (index < 0) return;
quintptr ptr = m_Combo->itemData(index).value<quintptr>();
Object* obj = reinterpret_cast<Object*>(ptr);
m_RefProp->SetReferencedObject(obj);
Q_EMIT updated();
}
////////////////////////////////////////////////////////////////////////////////
// PropertyEditor
PropertyEditor::PropertyEditor(QWidget* parent) : QWidget(parent), m_Object(nullptr), m_Context(nullptr) {
m_MainLayout = new QVBoxLayout(this);
m_MainLayout->setContentsMargins(0, 0, 0, 0);
m_ScrollArea = new QScrollArea(this);
@@ -488,7 +556,11 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) {
// widget = new RangePropertyWidget<float>(pflt, m_Container);
}
} else {
// Priority 2: Standard factory lookup
// Priority 2: Check for reference properties (SmartPointer<T>)
if (auto* refProp = dynamic_cast<::uLib::ReferencePropertyBase*>(prop)) {
widget = static_cast<QWidget*>(new ReferencePropertyWidget(refProp, m_Context, m_Container));
} else {
// Priority 3: Standard factory lookup
auto it = m_Factories.find(prop->GetTypeIndex());
if (it != m_Factories.end()) {
widget = it->second(prop, m_Container);
@@ -501,6 +573,7 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) {
widget->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")"));
}
}
}
if (widget) {
if (auto* propWidget = qobject_cast<PropertyWidgetBase*>(widget)) {

View File

@@ -4,6 +4,7 @@
#include <QWidget>
class QPushButton;
class QSlider;
class QComboBox;
#include <QLabel>
#include <QHBoxLayout>
#include <QVBoxLayout>
@@ -21,6 +22,8 @@ class QSlider;
#include "Math/Dense.h"
#include "Settings.h"
namespace uLib { class ObjectsContext; }
namespace uLib {
namespace Qt {
@@ -211,12 +214,28 @@ private:
QPushButton* m_Button;
};
class ReferencePropertyWidget : public PropertyWidgetBase {
Q_OBJECT
public:
ReferencePropertyWidget(ReferencePropertyBase* prop, ::uLib::ObjectsContext* context, QWidget* parent = nullptr);
virtual ~ReferencePropertyWidget();
private slots:
void onComboChanged(int index);
private:
void refreshCombo();
ReferencePropertyBase* m_RefProp;
::uLib::ObjectsContext* m_Context;
QComboBox* m_Combo;
Connection m_ContextConnection;
};
class PropertyEditor : public QWidget {
Q_OBJECT
public:
PropertyEditor(QWidget* parent = nullptr);
virtual ~PropertyEditor();
void setObject(uLib::Object* obj, bool displayOnly = false);
void setContext(uLib::ObjectsContext* context) { m_Context = context; }
template<typename T>
void registerFactory(std::function<QWidget*(PropertyBase*, QWidget*)> factory) {
m_Factories[std::type_index(typeid(T))] = factory;
@@ -228,6 +247,7 @@ signals:
private:
void clear();
uLib::Object* m_Object;
uLib::ObjectsContext* m_Context;
QVBoxLayout* m_MainLayout;
QScrollArea* m_ScrollArea;
QWidget* m_Container;

View File

@@ -113,6 +113,10 @@ void ViewportPane::setObject(uLib::Object* obj) {
}
}
void ViewportPane::setContext(uLib::ObjectsContext* context) {
m_displayEditor->setContext(context);
}
void ViewportPane::setViewport(QWidget* viewport, const QString& title) {
if (m_viewport) {
delete m_viewport;

View File

@@ -7,6 +7,7 @@
namespace uLib {
class Object;
class ObjectsContext;
namespace Qt { class PropertyEditor; }
namespace Vtk { class Viewport; }
}
@@ -30,6 +31,9 @@ public:
/** @brief Update the display properties for the given object. */
void setObject(uLib::Object* obj);
/** @brief Sets the context for reference property dropdowns. */
void setContext(uLib::ObjectsContext* context);
private slots:
void onCloseRequested();
void showContextMenu(const QPoint& pos);

View File

@@ -15,6 +15,15 @@
#include "Core/Archives.h"
#include "Core/Signal.h"
#include "Core/Object.h"
#include "Core/SmartPointer.h"
// Type traits for detecting SmartPointer<T>
namespace uLib {
template<typename T> struct is_smart_pointer : std::false_type {};
template<typename T> struct is_smart_pointer<SmartPointer<T>> : std::true_type {};
template<typename T> struct smart_pointer_element { using type = void; };
template<typename T> struct smart_pointer_element<SmartPointer<T>> { using type = T; };
} // namespace uLib
namespace uLib {
@@ -216,6 +225,109 @@ private:
} // namespace uLib
namespace uLib {
/**
* @brief Base class for reference properties (SmartPointer<T> fields).
* Provides a type-erased interface for getting/setting object references
* and checking type compatibility.
*/
class ReferencePropertyBase : public PropertyBase {
public:
virtual ~ReferencePropertyBase() {}
virtual Object* GetReferencedObject() const = 0;
virtual void SetReferencedObject(Object* obj) = 0;
virtual bool IsCompatible(Object* obj) const = 0;
virtual const char* GetReferenceTypeName() const = 0;
};
/**
* @brief Typed reference property for SmartPointer<T> fields.
* Filters context objects by dynamic_cast compatibility with T.
*/
template <typename T>
class ReferenceProperty : public ReferencePropertyBase {
public:
ReferenceProperty(Object* owner, const std::string& name, SmartPointer<T>& ref,
const std::string& units = "", const std::string& group = "")
: m_owner(owner), m_name(name), m_units(units), m_group(group), m_ref(ref), m_ReadOnly(false) {
if (m_owner) m_owner->RegisterProperty(this);
}
virtual ~ReferenceProperty() {}
// PropertyBase interface
virtual const std::string& GetName() const override { return m_name; }
virtual const char* GetTypeName() const override { return typeid(SmartPointer<T>).name(); }
virtual std::type_index GetTypeIndex() const override { return std::type_index(typeid(ReferencePropertyBase)); }
virtual const std::string& GetUnits() const override { return m_units; }
virtual void SetUnits(const std::string& units) override { m_units = units; }
virtual const std::string& GetGroup() const override { return m_group; }
virtual void SetGroup(const std::string& group) override { m_group = group; }
virtual bool IsReadOnly() const override { return m_ReadOnly; }
void SetReadOnly(bool ro) { m_ReadOnly = ro; }
virtual std::string GetValueAsString() const override {
T* ptr = m_ref.Get();
if (!ptr) return "(none)";
Object* obj = dynamic_cast<Object*>(ptr);
if (obj) {
std::string iname = obj->GetInstanceName();
if (!iname.empty()) return iname;
return obj->GetClassName();
}
return "(set)";
}
// ReferencePropertyBase interface
virtual Object* GetReferencedObject() const override {
return dynamic_cast<Object*>(m_ref.Get());
}
virtual void SetReferencedObject(Object* obj) override {
if (!obj) {
m_ref = SmartPointer<T>(nullptr);
this->Updated();
if (m_owner) m_owner->Updated();
return;
}
T* casted = dynamic_cast<T*>(obj);
if (casted) {
m_ref = SmartPointer<T>(casted);
this->Updated();
if (m_owner) m_owner->Updated();
}
}
virtual bool IsCompatible(Object* obj) const override {
return dynamic_cast<T*>(obj) != nullptr;
}
virtual const char* GetReferenceTypeName() const override {
return typeid(T).name();
}
// Serialization stubs
virtual void serialize(Archive::xml_oarchive & ar, const unsigned int v) override {}
virtual void serialize(Archive::xml_iarchive & ar, const unsigned int v) override {}
virtual void serialize(Archive::text_oarchive & ar, const unsigned int v) override {}
virtual void serialize(Archive::text_iarchive & ar, const unsigned int v) override {}
virtual void serialize(Archive::hrt_oarchive & ar, const unsigned int v) override {}
virtual void serialize(Archive::hrt_iarchive & ar, const unsigned int v) override {}
virtual void serialize(Archive::log_archive & ar, const unsigned int v) override {}
virtual void serialize(Archive::property_register_archive & ar, const unsigned int v) override {}
private:
Object* m_owner;
std::string m_name;
std::string m_units;
std::string m_group;
SmartPointer<T>& m_ref;
bool m_ReadOnly;
};
} // namespace uLib
namespace uLib {
namespace Archive {
@@ -267,7 +379,20 @@ public:
}
template<class T> void save_property_impl(const char* name, T& val, const char* units, bool hasRange, const T& minVal, const T& maxVal, bool isReadOnly) {
if (m_Object) {
if (!m_Object) return;
if constexpr (is_smart_pointer<T>::value) {
// SmartPointer<U> field: create a ReferenceProperty<U> for type-safe selection
using ElementT = typename smart_pointer_element<T>::type;
auto* p = new ReferenceProperty<ElementT>(m_Object, name, val, units ? units : "", GetCurrentGroup());
p->SetReadOnly(isReadOnly);
if (m_DisplayOnly) {
m_Object->RegisterDisplayProperty(p);
Object* obj = m_Object;
Object::connect(p, &Object::Updated, [obj]() { obj->Updated(); });
} else {
m_Object->RegisterDynamicProperty(p);
}
} else {
Property<T>* p = new Property<T>(m_Object, name, &val, units ? units : "", GetCurrentGroup());
set_range_helper(p, hasRange, minVal, maxVal, typename std::is_arithmetic<T>::type());
p->SetReadOnly(isReadOnly);

View File

@@ -12,6 +12,8 @@ ULIB_REGISTER_OBJECT(Material)
ULIB_REGISTER_OBJECT(Solid)
ULIB_REGISTER_OBJECT(TessellatedSolid)
ULIB_REGISTER_OBJECT(BoxSolid)
ULIB_REGISTER_OBJECT(LogicalVolume)
ULIB_REGISTER_OBJECT(PhysicalVolume)
ULIB_REGISTER_OBJECT(Scene)
ULIB_REGISTER_OBJECT(SkyPlaneEmitterPrimary)
ULIB_REGISTER_OBJECT(CylinderEmitterPrimary)

View File

@@ -163,7 +163,7 @@ protected:
G4VPhysicalVolume *m_Physical;
// ULIB_DECLARE_PROPERTIES(PhysicalVolume)
ULIB_DECLARE_PROPERTIES(PhysicalVolume)
};
@@ -173,9 +173,11 @@ protected:
class TessellatedSolid : public Solid {
public:
uLibTypeMacro(TessellatedSolid, Solid)
uLibTypeMacro(TessellatedSolid, Solid)
ULIB_SERIALIZE_ACCESS
public:
TessellatedSolid();
TessellatedSolid(const char *name);
@@ -191,6 +193,8 @@ public:
protected:
SmartPointer<TriangleMesh> m_Mesh;
G4TessellatedSolid *m_Solid;
//ULIB_DECLARE_PROPERTIES(TessellatedSolid)
};
@@ -198,8 +202,11 @@ protected:
//// BOX SOLID /////////////////////////////////////////////////////////////////
class BoxSolid : public Solid {
public:
uLibTypeMacro(BoxSolid, Solid)
ULIB_SERIALIZE_ACCESS
public:
BoxSolid();
BoxSolid(const char *name);
@@ -222,6 +229,8 @@ private:
SmartPointer<ContainerBox> m_ContainerBox;
G4Box *m_Solid;
ULIB_DECLARE_PROPERTIES(BoxSolid)
};