#include "PropertyWidgets.h" #include #include #include #include #include #include "Vtk/uLibVtkInterface.h" #include "Math/Units.h" #include "Math/Dense.h" #include #include #include #include #include #include "Settings.h" #include "Core/ObjectsContext.h" namespace uLib { namespace Qt { PropertyWidgetBase::PropertyWidgetBase(PropertyBase* prop, QWidget* parent) : QWidget(parent), m_BaseProperty(prop) { m_Layout = new QHBoxLayout(this); m_Layout->setContentsMargins(4, 2, 4, 2); std::string unit = prop->GetUnits(); QString labelText = QString::fromStdString(prop->GetName()); if (!unit.empty() && unit != "color") { auto dim = Settings::Instance().IdentifyDimension(unit); std::string pref = Settings::Instance().GetPreferredUnit(dim); if (!pref.empty()) { labelText += " [" + QString::fromStdString(pref) + "]"; } else { labelText += " [" + QString::fromStdString(unit) + "]"; } } m_Label = new QLabel(labelText, this); m_Label->setMinimumWidth(120); m_Layout->addWidget(m_Label); this->setEnabled(!prop->IsReadOnly()); } PropertyWidgetBase::~PropertyWidgetBase() { m_Connection.disconnect(); } // Helper for unit parsing double parseWithUnits(const QString& text, double* factorOut, QString* suffixOut) { static QRegularExpression re("^\\s*([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?)\\s*(_?[a-zA-Z]+)?\\s*$"); QRegularExpressionMatch match = re.match(text); if (!match.hasMatch()) return 0.0; double num = match.captured(1).toDouble(); QString unit = match.captured(3); double factor = factorOut ? *factorOut : 1.0; if (!unit.isEmpty()) { QString u = unit.startsWith('_') ? unit.mid(1) : unit; if (u == "m") factor = CLHEP::meter; else if (u == "cm") factor = CLHEP::centimeter; else if (u == "mm") factor = CLHEP::millimeter; else if (u == "um") factor = CLHEP::micrometer; else if (u == "nm") factor = CLHEP::nanometer; else if (u == "km") factor = CLHEP::kilometer; else if (u == "deg") factor = CLHEP::degree; else if (u == "rad") factor = CLHEP::radian; else if (u == "ns") factor = CLHEP::nanosecond; else if (u == "s") factor = CLHEP::second; else if (u == "ms") factor = CLHEP::millisecond; else if (u == "MeV") factor = CLHEP::megaelectronvolt; else if (u == "eV") factor = CLHEP::electronvolt; else if (u == "keV") factor = CLHEP::kiloelectronvolt; else if (u == "GeV") factor = CLHEP::gigaelectronvolt; else if (u == "TeV") factor = CLHEP::teraelectronvolt; if (suffixOut) *suffixOut = u; } else if (suffixOut) { // Reuse previous suffix if none provided, or empty } if (factorOut) *factorOut = factor; return num * factor; } // UnitLineEdit implementation UnitLineEdit::UnitLineEdit(QWidget* parent) : QLineEdit(parent), m_Value(0), m_Factor(1.0), m_Suffix(""), m_IsInteger(false) { connect(this, &QLineEdit::editingFinished, this, &UnitLineEdit::onEditingFinished); } void UnitLineEdit::setUnits(const QString& suffix, double factor) { m_Suffix = suffix; m_Factor = factor; updateText(); } void UnitLineEdit::setValue(double val) { if (m_Value != val) { m_Value = val; // Suffix heuristic ONLY if it was mm and no explicit unit was given? // Actually, if m_Suffix is empty or we have a specific one, we should respect it. // The original code had a heuristic, but it's better to let property decide. // Let's keep it ONLY if m_Suffix was mm (legacy behavior) if (!m_IsInteger && m_Suffix == "mm" && std::abs(val) >= 1000.0) { m_Suffix = "m"; m_Factor = CLHEP::meter; } updateText(); } } void UnitLineEdit::onEditingFinished() { double factor = m_Factor; QString suffix = m_Suffix; double parsedVal = parseWithUnits(text(), &factor, &suffix); if (m_IsInteger) { parsedVal = std::round(parsedVal); } if (m_Value != parsedVal) { m_Value = parsedVal; emit valueManualChanged(m_Value); } updateText(); } void UnitLineEdit::updateText() { QSignalBlocker blocker(this); QString s; if (m_IsInteger) { s = QString::number((int)m_Value); if (s.isEmpty()) s = "0"; } else { double displayVal = m_Value / m_Factor; s = QString::number(displayVal, 'g', 6); if (!s.contains('.') && !s.contains('e')) { s += ".0"; } } setText(s); } void UnitLineEdit::setIntegerOnly(bool integerOnly) { m_IsInteger = integerOnly; updateText(); } DoublePropertyWidget::DoublePropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Edit = new UnitLineEdit(this); std::string unit = prop->GetUnits(); if (!unit.empty()) { auto dim = Settings::Instance().IdentifyDimension(unit); std::string pref = Settings::Instance().GetPreferredUnit(dim); double factor = Settings::Instance().GetUnitFactor(pref); m_Edit->setUnits(QString::fromStdString(pref), factor); } m_Edit->setValue(prop->Get()); m_Layout->addWidget(m_Edit, 1); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set(val); emit updated(); }); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ m_Edit->setValue(m_Prop->Get()); }); } FloatPropertyWidget::FloatPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Edit = new UnitLineEdit(this); std::string unit = prop->GetUnits(); if (!unit.empty()) { auto dim = Settings::Instance().IdentifyDimension(unit); std::string pref = Settings::Instance().GetPreferredUnit(dim); double factor = Settings::Instance().GetUnitFactor(pref); m_Edit->setUnits(QString::fromStdString(pref), factor); } m_Edit->setValue(prop->Get()); m_Layout->addWidget(m_Edit, 1); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set((float)val); emit updated(); }); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ m_Edit->setValue((double)m_Prop->Get()); }); } IntPropertyWidget::IntPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Edit = new UnitLineEdit(this); m_Edit->setIntegerOnly(true); std::string unit = prop->GetUnits(); if (!unit.empty()) { auto dim = Settings::Instance().IdentifyDimension(unit); std::string pref = Settings::Instance().GetPreferredUnit(dim); double factor = Settings::Instance().GetUnitFactor(pref); m_Edit->setUnits(QString::fromStdString(pref), factor); } m_Edit->setValue(prop->Get()); m_Layout->addWidget(m_Edit, 1); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set((int)val); emit updated(); }); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ m_Edit->setValue((double)m_Prop->Get()); }); } BoolPropertyWidget::BoolPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_CheckBox = new QCheckBox(this); m_CheckBox->setChecked(prop->Get()); m_Layout->addWidget(m_CheckBox, 1); connect(m_CheckBox, &QCheckBox::toggled, [this](bool val){ if (m_Prop->Get() != val) { m_Prop->Set(val); emit updated(); } }); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ if (m_CheckBox->isChecked() != m_Prop->Get()) { QSignalBlocker blocker(m_CheckBox); m_CheckBox->setChecked(m_Prop->Get()); } }); } BoolPropertyWidget::~BoolPropertyWidget() {} RangePropertyWidget::RangePropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Slider = new QSlider(::Qt::Horizontal, this); m_Slider->setRange(0, 100); m_Slider->setMinimumWidth(80); m_Edit = new UnitLineEdit(this); m_Edit->setFixedWidth(50); m_Layout->addWidget(m_Slider, 1); m_Layout->addWidget(m_Edit, 0); connect(m_Slider, &QSlider::valueChanged, this, &RangePropertyWidget::onSliderChanged); connect(m_Edit, &UnitLineEdit::valueManualChanged, [this](double val){ m_Prop->Set(val); emit updated(); }); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ this->updateUi(); }); updateUi(); } RangePropertyWidget::~RangePropertyWidget() { m_Connection.disconnect(); } void RangePropertyWidget::updateUi() { double val = m_Prop->Get(); m_Edit->setValue(val); if (m_Prop->GetMax() != m_Prop->GetMin()) { int sliderVal = (int)((val - m_Prop->GetMin()) / (m_Prop->GetMax() - m_Prop->GetMin()) * 100.0); QSignalBlocker blocker(m_Slider); m_Slider->setValue(sliderVal); } } void RangePropertyWidget::onSliderChanged(int val) { double realVal = m_Prop->GetMin() + (val / 100.0) * (m_Prop->GetMax() - m_Prop->GetMin()); m_Prop->Set(realVal); emit updated(); } ColorPropertyWidget::ColorPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Button = new QPushButton(this); m_Button->setFixedWidth(60); this->updateButtonColor(); m_Layout->addWidget(m_Button, 0, ::Qt::AlignRight); connect(m_Button, &QPushButton::clicked, this, &ColorPropertyWidget::onClicked); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ this->updateButtonColor(); }); } ColorPropertyWidget::~ColorPropertyWidget() {} void ColorPropertyWidget::updateButtonColor() { Vector3d c = m_Prop->Get(); QColor color = QColor::fromRgbF(std::max(0.0, std::min(1.0, c.x())), std::max(0.0, std::min(1.0, c.y())), std::max(0.0, std::min(1.0, c.z()))); m_Button->setStyleSheet(QString("background-color: %1; border: 1px solid #555; height: 18px;").arg(color.name())); } void ColorPropertyWidget::onClicked() { Vector3d c = m_Prop->Get(); QColor current = QColor::fromRgbF(std::max(0.0, std::min(1.0, c.x())), std::max(0.0, std::min(1.0, c.y())), std::max(0.0, std::min(1.0, c.z()))); QColor selected = QColorDialog::getColor(current, this, "Select Color"); if (selected.isValid()) { m_Prop->Set(Vector3d(selected.redF(), selected.greenF(), selected.blueF())); emit updated(); } } StringPropertyWidget::StringPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_LineEdit = new QLineEdit(this); m_LineEdit->setText(QString::fromStdString(prop->Get())); m_Layout->addWidget(m_LineEdit, 1); connect(m_LineEdit, &QLineEdit::editingFinished, [this](){ std::string val = m_LineEdit->text().toStdString(); if (m_Prop->Get() != val) { m_Prop->Set(val); emit updated(); } }); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ if (m_LineEdit->text().toStdString() != m_Prop->Get()) { QSignalBlocker blocker(m_LineEdit); m_LineEdit->setText(QString::fromStdString(m_Prop->Get())); } }); } StringPropertyWidget::~StringPropertyWidget() {} FontPropertyWidget::FontPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Button = new QPushButton(this); m_Button->setMinimumWidth(100); this->updateButtonText(); m_Layout->addWidget(m_Button, 1); connect(m_Button, &QPushButton::clicked, this, &FontPropertyWidget::onClicked); m_Connection = uLib::Object::connect(m_Prop, &Property::Updated, [this](){ this->updateButtonText(); }); } FontPropertyWidget::~FontPropertyWidget() {} void FontPropertyWidget::updateButtonText() { FontConfig f = m_Prop->Get(); m_Button->setText(QString::fromStdString(f.family) + " " + QString::number(f.size)); } void FontPropertyWidget::onClicked() { FontConfig current = m_Prop->Get(); QFont font(QString::fromStdString(current.family), current.size); font.setBold(current.bold); font.setItalic(current.italic); bool ok; QFont selected = QFontDialog::getFont(&ok, font, this, "Select Font"); if (ok) { FontConfig newF(selected.family().toStdString(), selected.pointSize(), selected.bold(), selected.italic()); m_Prop->Set(newF); emit updated(); } } class GroupHeaderWidget : public QWidget { public: GroupHeaderWidget(const QString& name, QWidget* parent = nullptr) : QWidget(parent) { auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 8, 0, 4); auto* line = new QFrame(this); line->setFrameShape(QFrame::HLine); line->setFrameShadow(QFrame::Sunken); line->setStyleSheet("color: #555;"); layout->addWidget(line); auto* label = new QLabel(name, this); QFont font = label->font(); font.setBold(true); font.setPointSize(font.pointSize() + 1); label->setFont(font); label->setStyleSheet("color: #aaa; text-transform: uppercase;"); layout->addWidget(label); } }; class EnumPropertyWidget : public PropertyWidgetBase { PropertyBase* m_Prop; QComboBox* m_Combo; public: EnumPropertyWidget(PropertyBase* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_Combo = new QComboBox(this); const auto& labels = prop->GetEnumLabels(); for (const auto& label : labels) { m_Combo->addItem(QString::fromStdString(label)); } // Get initial value if (auto* p = dynamic_cast*>(prop)) { m_Combo->setCurrentIndex(p->Get()); connect(m_Combo, &QComboBox::currentIndexChanged, [this, p](int index){ p->Set(index); emit updated(); }); // Store connection in base m_Connection so it's auto-disconnected on destruction. m_Connection = uLib::Object::connect(p, &Property::Updated, [this, p](){ if (m_Combo->currentIndex() != p->Get()) { QSignalBlocker blocker(m_Combo); m_Combo->setCurrentIndex(p->Get()); } }); } m_Layout->addWidget(m_Combo, 1); } }; //////////////////////////////////////////////////////////////////////////////// // 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(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 (const auto& obj : objects) { if (m_RefProp->IsCompatible(obj.get())) { 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.get())); if (obj.get() == 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(); Object* obj = reinterpret_cast(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); m_ScrollArea->setWidgetResizable(true); m_MainLayout->addWidget(m_ScrollArea); m_Container = new QWidget(); m_ContainerLayout = new QVBoxLayout(m_Container); m_ContainerLayout->setAlignment(::Qt::AlignTop); m_ScrollArea->setWidget(m_Container); registerFactory([](PropertyBase* p, QWidget* parent){ return new DoublePropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new FloatPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new IntPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new BoolPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new StringPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new FontPropertyWidget(static_cast*>(p), parent); }); // Register EnumProperty specifically (needs to check type since it holds Property but is EnumProperty) m_Factories[std::type_index(typeid(EnumProperty))] = [](PropertyBase* p, QWidget* parent) { return new EnumPropertyWidget(p, parent); }; // Vector Registration registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); registerFactory([](PropertyBase* p, QWidget* parent){ return new VectorPropertyWidget(static_cast*>(p), parent); }); } PropertyEditor::~PropertyEditor() {} void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) { m_Object = obj; clear(); if (!obj) return; // Choose which properties to show const std::vector<::uLib::PropertyBase*>* props = &obj->GetProperties(); if (displayOnly) { if (auto* prop3d = dynamic_cast<::uLib::Vtk::Prop3D*>(obj)) { props = &prop3d->GetDisplayProperties(); } else { // If it's not a prop3d but displayOnly is requested, showing nothing or fallback? // Fallback: core properties. } } // Group properties by their group string std::map> groupedProps; std::vector groupOrder; for (auto* prop : *props) { std::string group = prop->GetGroup(); if (groupedProps.find(group) == groupedProps.end()) { groupOrder.push_back(group); } groupedProps[group].push_back(prop); } for (const auto& groupName : groupOrder) { if (!groupName.empty()) { m_ContainerLayout->addWidget(new GroupHeaderWidget(QString::fromStdString(groupName), m_Container)); } for (auto* prop : groupedProps[groupName]) { QWidget* widget = nullptr; // Priority 1: Check if it provides enum labels if (!prop->GetEnumLabels().empty()) { widget = new EnumPropertyWidget(prop, m_Container); } else if (prop->GetUnits() == "color") { // Color Picker for Vector3d if (auto* pvec = dynamic_cast*>(prop)) { widget = new ColorPropertyWidget(pvec, m_Container); } } else if (prop->HasRange()) { // Slider for ranged doubles if (auto* pdbl = dynamic_cast*>(prop)) { widget = new RangePropertyWidget(pdbl, m_Container); } else if (auto* pflt = dynamic_cast*>(prop)) { // widget = new RangePropertyWidget(pflt, m_Container); } } else { // Priority 2: Check for reference properties (SmartPointer) if (auto* refProp = dynamic_cast<::uLib::ReferencePropertyBase*>(prop)) { widget = static_cast(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); } else { // Debug info for unknown types std::cout << "PropertyEditor: No factory for " << prop->GetQualifiedName() << " (Type: " << prop->GetTypeName() << ")" << std::endl; widget = new PropertyWidgetBase(prop, m_Container); widget->layout()->addWidget(new QLabel("(Read-only: " + QString::fromStdString(prop->GetValueAsString()) + ")")); } } } if (widget) { if (auto* propWidget = qobject_cast(widget)) { connect(propWidget, &PropertyWidgetBase::updated, [this, prop](){ emit propertyUpdated(prop); }); } if (!groupName.empty()) { // Indent grouped properties widget->setContentsMargins(16, 0, 0, 0); } m_ContainerLayout->addWidget(widget); } } } m_ContainerLayout->addStretch(1); } void PropertyEditor::clear() { QLayoutItem* item; while ((item = m_ContainerLayout->takeAt(0)) != nullptr) { delete item->widget(); delete item; } } } // namespace Qt } // namespace uLib