diff --git a/app/gcompose/src/PropertyWidgets.cpp b/app/gcompose/src/PropertyWidgets.cpp index 9d13fea..1c586c5 100644 --- a/app/gcompose/src/PropertyWidgets.cpp +++ b/app/gcompose/src/PropertyWidgets.cpp @@ -7,6 +7,10 @@ #include "Vtk/uLibVtkInterface.h" #include "Math/Units.h" #include "Math/Dense.h" +#include +#include +#include +#include #include "Settings.h" namespace uLib { @@ -19,7 +23,7 @@ PropertyWidgetBase::PropertyWidgetBase(PropertyBase* prop, QWidget* parent) std::string unit = prop->GetUnits(); QString labelText = QString::fromStdString(prop->GetName()); - if (!unit.empty()) { + if (!unit.empty() && unit != "color") { auto dim = Settings::Instance().IdentifyDimension(unit); std::string pref = Settings::Instance().GetPreferredUnit(dim); if (!pref.empty()) { @@ -203,6 +207,76 @@ BoolPropertyWidget::BoolPropertyWidget(Property* prop, QWidget* parent) } 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); }); + + m_Connection = uLib::Object::connect(m_Prop, &Property::PropertyChanged, [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); +} + +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::PropertyChanged, [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())); + } +} + StringPropertyWidget::StringPropertyWidget(Property* prop, QWidget* parent) : PropertyWidgetBase(prop, parent), m_Prop(prop) { m_LineEdit = new QLineEdit(this); @@ -358,6 +432,18 @@ void PropertyEditor::setObject(::uLib::Object* obj, bool displayOnly) { // 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: Standard factory lookup auto it = m_Factories.find(prop->GetTypeIndex()); diff --git a/app/gcompose/src/PropertyWidgets.h b/app/gcompose/src/PropertyWidgets.h index 4891432..7374389 100644 --- a/app/gcompose/src/PropertyWidgets.h +++ b/app/gcompose/src/PropertyWidgets.h @@ -2,6 +2,8 @@ #define PROPERTY_WIDGETS_H #include +class QPushButton; +class QSlider; #include #include #include @@ -141,6 +143,20 @@ private: UnitLineEdit* m_Edits[Size]; }; +class RangePropertyWidget : public PropertyWidgetBase { + Q_OBJECT +public: + RangePropertyWidget(Property* prop, QWidget* parent = nullptr); + virtual ~RangePropertyWidget(); +private slots: + void onSliderChanged(int val); +private: + void updateUi(); + Property* m_Prop; + QSlider* m_Slider; + UnitLineEdit* m_Edit; +}; + class BoolPropertyWidget : public PropertyWidgetBase { Q_OBJECT public: @@ -151,6 +167,19 @@ private: QCheckBox* m_CheckBox; }; +class ColorPropertyWidget : public PropertyWidgetBase { + Q_OBJECT +public: + ColorPropertyWidget(Property* prop, QWidget* parent = nullptr); + virtual ~ColorPropertyWidget(); +private slots: + void onClicked(); +private: + void updateButtonColor(); + Property* m_Prop; + QPushButton* m_Button; +}; + class StringPropertyWidget : public PropertyWidgetBase { Q_OBJECT public: diff --git a/src/Core/Property.h b/src/Core/Property.h index 0696818..fe30fbe 100644 --- a/src/Core/Property.h +++ b/src/Core/Property.h @@ -36,6 +36,13 @@ public: } virtual const std::string& GetGroup() const = 0; virtual void SetGroup(const std::string& group) = 0; + + virtual bool HasRange() const { return false; } + virtual double GetMin() const { return 0; } + virtual double GetMax() const { return 0; } + virtual bool HasDefault() const { return false; } + virtual std::string GetDefaultValueAsString() const { return ""; } + std::string GetQualifiedName() const { if (GetGroup().empty()) return GetName(); return GetGroup() + "." + GetName(); @@ -63,7 +70,8 @@ class Property : public PropertyBase { public: // PROXY: Use an existing variable as back-end storage Property(Object* owner, const std::string& name, T* valuePtr, const std::string& units = "", const std::string& group = "") - : m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(valuePtr), m_own(false) { + : m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(valuePtr), m_own(false), + m_HasRange(false), m_HasDefault(false) { if (m_owner) { m_owner->RegisterProperty(this); } @@ -71,7 +79,8 @@ public: // MANAGED: Create and own internal storage Property(Object* owner, const std::string& name, const T& defaultValue = T(), const std::string& units = "", const std::string& group = "") - : m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(new T(defaultValue)), m_own(true) { + : m_owner(owner), m_name(name), m_units(units), m_group(group), m_value(new T(defaultValue)), m_own(true), + m_HasRange(false), m_HasDefault(true), m_Default(defaultValue) { if (m_owner) { m_owner->RegisterProperty(this); } @@ -103,15 +112,61 @@ public: // Accessors const T& Get() const { return *m_value; } + template + typename std::enable_if::value, void>::type + ValidateT(T& val) { + if (m_HasRange) { + if (val < m_Min) val = m_Min; + if (val > m_Max) val = m_Max; + } + } + + template + typename std::enable_if::value, void>::type + ValidateT(T& val) { + } + void Set(const T& value) { - if (*m_value != value) { - *m_value = value; + T val = value; + ValidateT(val); + if (*m_value != val) { + *m_value = val; ULIB_SIGNAL_EMIT(Property::PropertyChanged); this->Updated(); if (m_owner) m_owner->Updated(); } } + void SetRange(const T& min, const T& max) { m_Min = min; m_Max = max; m_HasRange = true; } + void SetDefault(const T& def) { m_Default = def; m_HasDefault = true; } + + virtual bool HasRange() const override { return m_HasRange; } + + template + typename std::enable_if::value, double>::type + GetMinT() const { return (double)m_Min; } + + template + typename std::enable_if::value, double>::type + GetMinT() const { return 0.0; } + + template + typename std::enable_if::value, double>::type + GetMaxT() const { return (double)m_Max; } + + template + typename std::enable_if::value, double>::type + GetMaxT() const { return 0.0; } + + virtual double GetMin() const override { return GetMinT(); } + virtual double GetMax() const override { return GetMaxT(); } + + virtual bool HasDefault() const override { return m_HasDefault; } + virtual std::string GetDefaultValueAsString() const override { + try { return boost::lexical_cast(m_Default); } + catch (...) { return ""; } + } + // Operators for seamless usage operator const T&() const { return *m_value; } Property& operator=(const T& value) { @@ -149,6 +204,11 @@ private: T* m_value; bool m_own; Object* m_owner; + bool m_HasRange; + T m_Min; + T m_Max; + bool m_HasDefault; + T m_Default; }; /** @@ -242,6 +302,8 @@ public: void save_override(const boost::serialization::hrp &t) { if (m_Object) { Property* p = new Property(m_Object, t.name(), &const_cast&>(t).value(), t.units() ? t.units() : "", GetCurrentGroup()); + if (t.has_range()) p->SetRange(t.min_val(), t.max_val()); + if (t.has_default()) p->SetDefault(t.default_val()); m_Object->RegisterDynamicProperty(p); } } diff --git a/src/Core/Serializable.h b/src/Core/Serializable.h index e5e620d..03bcfe2 100644 --- a/src/Core/Serializable.h +++ b/src/Core/Serializable.h @@ -77,15 +77,30 @@ class hrp : public boost::serialization::wrapper_traits> { const char *m_name; const char *m_units; T &m_value; + bool m_has_range; + T m_min; + T m_max; + bool m_has_default; + T m_default; public: - explicit hrp(const char *name_, T &t, const char* units_ = nullptr) : m_name(name_), m_units(units_), m_value(t) {} + explicit hrp(const char *name_, T &t, const char* units_ = nullptr) + : m_name(name_), m_units(units_), m_value(t), m_has_range(false), m_has_default(false) {} + + hrp& range(const T& min_val, const T& max_val) { m_min = min_val; m_max = max_val; m_has_range = true; return *this; } + hrp& set_default(const T& def_val) { m_default = def_val; m_has_default = true; return *this; } const char *name() const { return this->m_name; } const char *units() const { return this->m_units; } T &value() { return this->m_value; } const T &const_value() const { return this->m_value; } + bool has_range() const { return m_has_range; } + const T& min_val() const { return m_min; } + const T& max_val() const { return m_max; } + bool has_default() const { return m_has_default; } + const T& default_val() const { return m_default; } + BOOST_SERIALIZATION_SPLIT_MEMBER() template @@ -110,16 +125,23 @@ class hrp_enum : public boost::serialization::wrapper_traits> { const char *m_units; T &m_value; std::vector m_labels; + bool m_has_default; + T m_default; public: explicit hrp_enum(const char *name_, T &t, const std::vector& labels, const char* units_ = nullptr) - : m_name(name_), m_units(units_), m_value(t), m_labels(labels) {} + : m_name(name_), m_units(units_), m_value(t), m_labels(labels), m_has_default(false) {} + + hrp_enum& set_default(const T& def_val) { m_default = def_val; m_has_default = true; return *this; } const char *name() const { return this->m_name; } const char *units() const { return this->m_units; } T &value() { return this->m_value; } const std::vector& labels() const { return m_labels; } + bool has_default() const { return m_has_default; } + const T& default_val() const { return m_default; } + BOOST_SERIALIZATION_SPLIT_MEMBER() template diff --git a/src/Vtk/uLibVtkInterface.cxx b/src/Vtk/uLibVtkInterface.cxx index d390f58..b664240 100644 --- a/src/Vtk/uLibVtkInterface.cxx +++ b/src/Vtk/uLibVtkInterface.cxx @@ -86,13 +86,13 @@ public: m_ShowBoundingBox(false), m_ShowScaleMeasures(false), m_Representation(Puppet::Surface), - m_Opacity(-1.0), + m_Opacity(1.0), m_Selectable(true), m_Selected(false), m_Visibility(true), m_Dragable(true) { - m_Color[0] = m_Color[1] = m_Color[2] = -1.0; + m_Color = Vector3d(-1, -1, -1); } ~PuppetData() { @@ -111,7 +111,7 @@ public: bool m_ShowBoundingBox; bool m_ShowScaleMeasures; int m_Representation; - double m_Color[3]; + Vector3d m_Color; double m_Opacity; bool m_Selectable; @@ -137,8 +137,9 @@ public: actor->GetProperty()->SetEdgeVisibility(0); } } - if (m_Color[0] != -1.0) { - actor->GetProperty()->SetColor(m_Color); + if (m_Color.x() != -1.0) { + double c[3] = {m_Color.x(), m_Color.y(), m_Color.z()}; + actor->GetProperty()->SetColor(c); } if (m_Opacity != -1.0) { @@ -280,8 +281,11 @@ void Puppet::SetProp(vtkProp *prop) pd->m_Representation = vp->GetRepresentation(); if (pd->m_Opacity < 0) pd->m_Opacity = vp->GetOpacity(); - if (pd->m_Color[0] < 0) - vp->GetColor(pd->m_Color); + if (pd->m_Color.x() < 0) { + double c[3]; + vp->GetColor(c); + pd->m_Color = Vector3d(c[0], c[1], c[2]); + } } } } @@ -622,10 +626,8 @@ struct AppearanceProxy { PuppetData* pd; template void serialize(Archive & ar, const unsigned int version) { - ar & boost::serialization::make_hrp("ColorR", pd->m_Color[0]); - ar & boost::serialization::make_hrp("ColorG", pd->m_Color[1]); - ar & boost::serialization::make_hrp("ColorB", pd->m_Color[2]); - ar & boost::serialization::make_hrp("Opacity", pd->m_Opacity); + ar & boost::serialization::make_hrp("Color", pd->m_Color, "color"); + ar & boost::serialization::make_hrp("Opacity", pd->m_Opacity).range(0.0, 1.0).set_default(1.0); ar & boost::serialization::make_hrp_enum("Representation", pd->m_Representation, {"Points", "Wireframe", "Surface", "SurfaceWithEdges", "Volume", "Outline", "Slice"}); ar & boost::serialization::make_hrp("Visibility", pd->m_Visibility); ar & boost::serialization::make_hrp("Pickable", pd->m_Selectable); diff --git a/src/Vtk/uLibVtkInterface.h b/src/Vtk/uLibVtkInterface.h index 74ae5ee..b0577f0 100644 --- a/src/Vtk/uLibVtkInterface.h +++ b/src/Vtk/uLibVtkInterface.h @@ -177,6 +177,9 @@ public: void save_override(const boost::serialization::hrp &t) { if (m_Puppet) { uLib::Property* p = new uLib::Property(m_Puppet, t.name(), &const_cast&>(t).value(), t.units() ? t.units() : "", GetCurrentGroup()); + if (t.has_range()) p->SetRange(t.min_val(), t.max_val()); + if (t.has_default()) p->SetDefault(t.default_val()); + m_Puppet->RegisterDisplayProperty(p); Vtk::Puppet* puppet = m_Puppet; uLib::Object::connect(p, &uLib::PropertyBase::Updated, [puppet](){ puppet->Update(); });