From 4257d16d32ead59afab4cd208951830a238df33f Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 02:48:26 +0300 Subject: [PATCH 01/18] PM-140 Added experimental API section Introduces clearProperties() and propertyNames() methods to PropertyGrid, allowing clients to reset the property grid and retrieve all property names. Implements supporting clearModel() and getPropertiesNames() methods in PropertyGridTreeModel for internal functionality. --- src/PropertyGrid.cpp | 10 ++++++++++ src/PropertyGrid.h | 9 +++++++-- src/PropertyGridTreeModel.cpp | 11 +++++++++++ src/PropertyGridTreeModel.h | 8 ++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/PropertyGrid.cpp b/src/PropertyGrid.cpp index 6028889..f61b7af 100644 --- a/src/PropertyGrid.cpp +++ b/src/PropertyGrid.cpp @@ -768,6 +768,16 @@ PropertyContext PropertyGrid::getPropertyContext(const QString &propertyName) co return internal::PropertyContextPrivate::invalidContext(); } +void PropertyGrid::clearProperties() +{ + d->m_model.clearModel(); +} + +QStringList PropertyGrid::propertyNames() const +{ + return d->m_model.getPropertiesNames(); +} + void PropertyGrid::addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor) { d->m_propertyEditors.emplace(typeId, std::move(editor)); diff --git a/src/PropertyGrid.h b/src/PropertyGrid.h index 8c8bedb..8c395f9 100644 --- a/src/PropertyGrid.h +++ b/src/PropertyGrid.h @@ -1,8 +1,8 @@ #ifndef PROPERTYGRID_H #define PROPERTYGRID_H -#include "QtCompat_p.h" #include "PropertyEditor.h" +#include "QtCompat_p.h" #include @@ -51,11 +51,16 @@ class PropertyGrid : public QWidget PropertyContext getPropertyContext(const QString &propertyName) const; +public: /* EXPERIMENTAL API */ + /**/ + void clearProperties(); // Requested: 2 + QStringList propertyNames() const; // Requested: 2 + signals: void propertyValueChanged(const PM::PropertyContext &context); private: // stable internal functions - void addPropertyEditor_impl(TypeId, std::unique_ptr &&editor); + void addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor); private: PropertyGridPrivate *d; diff --git a/src/PropertyGridTreeModel.cpp b/src/PropertyGridTreeModel.cpp index 1ae1f99..45c032c 100644 --- a/src/PropertyGridTreeModel.cpp +++ b/src/PropertyGridTreeModel.cpp @@ -173,6 +173,17 @@ QModelIndex internal::PropertyGridTreeModel::getItemIndex(PropertyGridTreeItem * return createIndex(item->indexInParent(m_showCategories), 0, item); } +void internal::PropertyGridTreeModel::clearModel() +{ + beginResetModel(); + { + m_categoriesMap.clear(); + m_propertiesMap.clear(); + m_rootItem->children.clear(); + } + endResetModel(); +} + internal::PropertyGridTreeItem *internal::PropertyGridTreeModel::getCategoryItem(const QString &category) { if (m_categoriesMap.contains(category)) diff --git a/src/PropertyGridTreeModel.h b/src/PropertyGridTreeModel.h index ae3345e..af4d58c 100644 --- a/src/PropertyGridTreeModel.h +++ b/src/PropertyGridTreeModel.h @@ -56,6 +56,9 @@ namespace internal PropertyGridTreeItem *getItem(const QModelIndex &index) const; // FIXME: should this be public?!! QModelIndex getItemIndex(PropertyGridTreeItem *item) const; + void clearModel(); + QStringList getPropertiesNames() const; + private: bool m_showCategories; PropertyGridTreeItem *m_rootItem; @@ -66,4 +69,9 @@ namespace internal } // namespace internal } // namespace PM +inline QStringList PM::internal::PropertyGridTreeModel::getPropertiesNames() const +{ + return m_propertiesMap.keys(); +} + #endif // PROPERTYGRIDTREEMODEL_H From 7815966f7dbd58783a24b03a71edf1a039efc15e Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:00:02 +0300 Subject: [PATCH 02/18] PM-140 Add support for removing property editors Introduces PropertyGrid::removePropertyEditor, allowing property editors to be removed by type. Also refactors PropertyGridTreeModel to add an update() method for model refresh, and uses it in setShowCategories for consistency. --- src/PropertyGrid.cpp | 15 +++++++++++++++ src/PropertyGrid.h | 10 ++++++++++ src/PropertyGridTreeModel.cpp | 21 +++++++++++++-------- src/PropertyGridTreeModel.h | 2 ++ 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/PropertyGrid.cpp b/src/PropertyGrid.cpp index f61b7af..9ccd113 100644 --- a/src/PropertyGrid.cpp +++ b/src/PropertyGrid.cpp @@ -778,6 +778,21 @@ QStringList PropertyGrid::propertyNames() const return d->m_model.getPropertiesNames(); } +void PropertyGrid::removePropertyEditor_impl(TypeId typeId) +{ + d->m_propertyEditors.erase(typeId); + + const QStringList names = propertyNames(); + for (const QString &propertyName : names) + { + const PropertyContext context = getPropertyContext(propertyName); + internal::PropertyGridTreeItem *item = d->m_model.getPropertyItem(propertyName); + const QModelIndex propertyIndex = d->m_model.getItemIndex(item); + + d->updatePropertyValue(propertyIndex, context.value()); + } +} + void PropertyGrid::addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor) { d->m_propertyEditors.emplace(typeId, std::move(editor)); diff --git a/src/PropertyGrid.h b/src/PropertyGrid.h index 8c395f9..f544033 100644 --- a/src/PropertyGrid.h +++ b/src/PropertyGrid.h @@ -56,10 +56,14 @@ class PropertyGrid : public QWidget void clearProperties(); // Requested: 2 QStringList propertyNames() const; // Requested: 2 + template ()>> + void removePropertyEditor(); + signals: void propertyValueChanged(const PM::PropertyContext &context); private: // stable internal functions + void removePropertyEditor_impl(TypeId typeId); void addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor); private: @@ -67,6 +71,12 @@ class PropertyGrid : public QWidget }; } // namespace PM +template +inline void PM::PropertyGrid::removePropertyEditor() +{ + removePropertyEditor_impl(internal::getTypeId()); +} + template inline void PM::PropertyGrid::addPropertyEditor() { diff --git a/src/PropertyGridTreeModel.cpp b/src/PropertyGridTreeModel.cpp index 45c032c..a5f0cd1 100644 --- a/src/PropertyGridTreeModel.cpp +++ b/src/PropertyGridTreeModel.cpp @@ -114,14 +114,7 @@ void internal::PropertyGridTreeModel::setShowCategories(bool newShowCategories) m_showCategories = newShowCategories; - beginResetModel(); - - // Notify about the structural change - emit layoutAboutToBeChanged(); - emit dataChanged(createIndex(0, 0), createIndex(rowCount(), columnCount())); - emit layoutChanged(); - - endResetModel(); + update(); } internal::PropertyGridTreeItem *internal::PropertyGridTreeModel::rootItem() const @@ -184,6 +177,18 @@ void internal::PropertyGridTreeModel::clearModel() endResetModel(); } +void internal::PropertyGridTreeModel::update() +{ + beginResetModel(); + + // Notify about the structural change + emit layoutAboutToBeChanged(); + emit dataChanged(createIndex(0, 0), createIndex(rowCount(), columnCount())); + emit layoutChanged(); + + endResetModel(); +} + internal::PropertyGridTreeItem *internal::PropertyGridTreeModel::getCategoryItem(const QString &category) { if (m_categoriesMap.contains(category)) diff --git a/src/PropertyGridTreeModel.h b/src/PropertyGridTreeModel.h index af4d58c..e0083d6 100644 --- a/src/PropertyGridTreeModel.h +++ b/src/PropertyGridTreeModel.h @@ -59,6 +59,8 @@ namespace internal void clearModel(); QStringList getPropertiesNames() const; + void update(); + private: bool m_showCategories; PropertyGridTreeItem *m_rootItem; From 76f1100ab86e64dff7c3b72e92e81c1a85356431 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:04:03 +0300 Subject: [PATCH 03/18] PM-140 Move addProperty template implementation Relocated the implementation of the addProperty template function from inline in the class definition to an out-of-class inline definition. This improves code organization and maintains template visibility for header-only usage. --- src/PropertyGrid.h | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/PropertyGrid.h b/src/PropertyGrid.h index f544033..8ab177a 100644 --- a/src/PropertyGrid.h +++ b/src/PropertyGrid.h @@ -30,14 +30,7 @@ class PropertyGrid : public QWidget // @@ CONVENIENCE template - void addProperty(const QString &name, const QVariant &value, const Attributes &...attributes) - { - if (!value.isValid()) - qWarning() << "PropertyGrid::addProperty(): Cannot infer the type of property" << name - << "because the provided QVariant value is invalid."; - - addProperty(Property(name, internal::getVariantTypeId(value), attributes...), value); - } + void addProperty(const QString &name, const QVariant &value, const Attributes &...attributes); // TODO: change to return false if a property editor returns subProperties list with invalid names?!! template ()>> @@ -53,12 +46,12 @@ class PropertyGrid : public QWidget public: /* EXPERIMENTAL API */ /**/ - void clearProperties(); // Requested: 2 - QStringList propertyNames() const; // Requested: 2 - template ()>> void removePropertyEditor(); + void clearProperties(); // Requested: 2 + QStringList propertyNames() const; // Requested: 2 + signals: void propertyValueChanged(const PM::PropertyContext &context); @@ -71,6 +64,15 @@ class PropertyGrid : public QWidget }; } // namespace PM +template +inline void PM::PropertyGrid::addProperty(const QString &name, const QVariant &value, const Attributes &...attributes) +{ + if (!value.isValid()) + qWarning() << "PropertyGrid::addProperty(): Cannot infer the type of property" << name << "because the provided QVariant value is invalid."; + + addProperty(Property(name, internal::getVariantTypeId(value), attributes...), value); +} + template inline void PM::PropertyGrid::removePropertyEditor() { From 927979ed2904fd4843cebc2b268fd47918aa6116 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:31:22 +0300 Subject: [PATCH 04/18] PM-140 Refactor PropertyContext to standalone header file Moved PropertyContext and PropertyContextPrivate to their own files, decoupling them from PropertyEditor and PropertyGrid internals. Updated all relevant includes and usages to reference the new module, improving code organization and maintainability. --- src/CMakeLists.txt | 6 +- src/PropertyContext.cpp | 102 ++++++++++++++++++++++++++++++++++ src/PropertyContext.h | 65 ++++++++++++++++++++++ src/PropertyContext_p.h | 32 +++++++++++ src/PropertyEditor.cpp | 36 +----------- src/PropertyEditor.h | 55 +----------------- src/PropertyGrid.cpp | 70 ++--------------------- src/PropertyGridTreeItem.cpp | 10 ++-- src/PropertyGridTreeItem.h | 2 +- src/PropertyGridTreeModel.cpp | 6 +- src/PropertyGridTreeModel.h | 2 +- src/PropertyGrid_p.h | 24 -------- 12 files changed, 220 insertions(+), 190 deletions(-) create mode 100644 src/PropertyContext.cpp create mode 100644 src/PropertyContext.h create mode 100644 src/PropertyContext_p.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f5e61bc..d5af314 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,9 +12,10 @@ find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Widgets REQUIRED) set(PUBLIC_HEADERS Property.h + QtCompat_p.h PropertyGrid.h PropertyEditor.h - QtCompat_p.h + PropertyContext.h ) add_library(PmPropertyGrid STATIC @@ -27,6 +28,9 @@ add_library(PmPropertyGrid STATIC PropertyGridTreeItem.cpp PropertyGridTreeModel.h PropertyGridTreeModel.cpp + PropertyContext_p.h + PropertyContext.cpp + ${PUBLIC_HEADERS} ) diff --git a/src/PropertyContext.cpp b/src/PropertyContext.cpp new file mode 100644 index 0000000..1b9b5a3 --- /dev/null +++ b/src/PropertyContext.cpp @@ -0,0 +1,102 @@ +#include "PropertyContext.h" +#include "PropertyContext_p.h" + +#include "PropertyGrid.h" + +using namespace PM; + +PropertyContext &PropertyContextPrivate::invalidContext() +{ + static PropertyContext result; + + return result; +} + +PropertyContext PropertyContextPrivate::createContext(const Property &property) +{ + return PropertyContext(property, QVariant(), nullptr, nullptr); +} + +PropertyContext PropertyContextPrivate::createContext(const QVariant &value, bool valid) +{ + PropertyContext result; + result.m_value = value; + result.m_isValid = valid; + + return result; +} + +PropertyContext PropertyContextPrivate::createContext(const PropertyContext &other, const QVariant &newValue) +{ + PropertyContext result = other; + result.m_value = newValue; + + return result; +} + +PropertyContext PropertyContextPrivate::createContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid) +{ + return PropertyContext(property, value, object, propertyGrid); +} + +void PropertyContextPrivate::setValue(PropertyContext &context, const QVariant &value) +{ + context.m_value = value; + notifyValueChanged(context, value); +} + +PropertyContextPrivate::valueChangedSlot_t PropertyContextPrivate::defaultValueChangedSlot() +{ + static valueChangedSlot_t result = [](const QVariant &) {}; + + return result; +} + +void PropertyContextPrivate::disconnectValueChangedSlot(PropertyContext &context) +{ + context.m_valueChangedSlot = defaultValueChangedSlot(); +} + +void PropertyContextPrivate::connectValueChangedSlot(PropertyContext &context, const valueChangedSlot_t &slot) +{ + context.m_valueChangedSlot = slot; +} + +void PropertyContextPrivate::notifyValueChanged(const PropertyContext &context, const QVariant &newValue) +{ + context.m_valueChangedSlot(newValue); +} + +Property PropertyContext::property() const +{ + return m_property; +} + +QVariant PropertyContext::value() const +{ + return m_value; +} + +bool PropertyContext::isValid() const +{ + return m_isValid; +} + +PropertyGrid *PropertyContext::propertyGrid() const +{ + return m_propertyGrid; +} + +PropertyContext::PropertyContext() : PropertyContext(Property(), QVariant(), nullptr, nullptr) +{ +} + +PropertyContext::PropertyContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid) : + m_property(property), + m_value(value), + m_object(object), + m_propertyGrid(propertyGrid), + m_isValid(!property.name().isEmpty()), + m_valueChangedSlot(PropertyContextPrivate::defaultValueChangedSlot()) +{ +} diff --git a/src/PropertyContext.h b/src/PropertyContext.h new file mode 100644 index 0000000..2c6a1ee --- /dev/null +++ b/src/PropertyContext.h @@ -0,0 +1,65 @@ +#ifndef PROPERTYCONTEXT_H +#define PROPERTYCONTEXT_H + +#include "Property.h" + +#include + +namespace PM +{ +class PropertyGrid; +class PropertyContextPrivate; + +class PropertyContext +{ + friend class PM::PropertyContextPrivate; + +public: + Property property() const; + QVariant value() const; + + bool isValid() const; + + template + T object() const // WARNING: this function doesn't perform any checks for type-safety + { + return getObjectValue(std::is_pointer()); + } + + PropertyGrid *propertyGrid() const; + +private: // TODO: wouldn't this cause problems when writing unit tests for user property editors? + PropertyContext(); + PropertyContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid); + + // function for pointer types + template + T getObjectValue(std::true_type) const + { + return static_cast(m_object); + } + + // function for non-pointer types + template + T getObjectValue(std::false_type) const + { + T *result = static_cast(m_object); + if (result == nullptr) + return T(); + + return *result; + } + +private: + Property m_property; + QVariant m_value; + void *m_object; // TODO: use std::any + QPointer m_propertyGrid; + + // Meta values + bool m_isValid; + std::function m_valueChangedSlot; +}; +} // namespace PM + +#endif // PROPERTYCONTEXT_H diff --git a/src/PropertyContext_p.h b/src/PropertyContext_p.h new file mode 100644 index 0000000..1b4952b --- /dev/null +++ b/src/PropertyContext_p.h @@ -0,0 +1,32 @@ +#ifndef PROPERTYCONTEXT_P_H +#define PROPERTYCONTEXT_P_H + +#include "PropertyContext.h" + +namespace PM +{ +class PropertyContextPrivate +{ +public: + using valueChangedSlot_t = std::function; + +public: + static PropertyContext &invalidContext(); + + static PropertyContext createContext(const Property &property); + static PropertyContext createContext(const QVariant &value, bool valid = true); + static PropertyContext createContext(const PropertyContext &other, const QVariant &newValue); + static PropertyContext createContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid); + + static void setValue(PropertyContext &context, const QVariant &value); + + static valueChangedSlot_t defaultValueChangedSlot(); + + static void disconnectValueChangedSlot(PropertyContext &context); + static void connectValueChangedSlot(PropertyContext &context, const valueChangedSlot_t &slot); + + static void notifyValueChanged(const PropertyContext &context, const QVariant &newValue); +}; +} // namespace PM + +#endif // PROPERTYCONTEXT_P_H diff --git a/src/PropertyEditor.cpp b/src/PropertyEditor.cpp index 1ed4310..82095ce 100644 --- a/src/PropertyEditor.cpp +++ b/src/PropertyEditor.cpp @@ -1,6 +1,6 @@ #include "PropertyEditor.h" -#include "PropertyGrid_p.h" +#include "PropertyContext_p.h" #include "QtCompat_p.h" #include @@ -39,40 +39,6 @@ const QString &boolToString(bool value) return value ? trueKey : falseKey; } -Property PropertyContext::property() const -{ - return m_property; -} - -QVariant PropertyContext::value() const -{ - return m_value; -} - -bool PropertyContext::isValid() const -{ - return m_isValid; -} - -PropertyGrid *PropertyContext::propertyGrid() const -{ - return m_propertyGrid; -} - -PropertyContext::PropertyContext() : PropertyContext(Property(), QVariant(), nullptr, nullptr) -{ -} - -PropertyContext::PropertyContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid) : - m_property(property), - m_value(value), - m_object(object), - m_propertyGrid(propertyGrid), - m_isValid(!property.name().isEmpty()), - m_valueChangedSlot(PropertyContextPrivate::defaultValueChangedSlot()) -{ -} - bool PropertyEditor::canHandle(const PropertyContext &context) const { return true; diff --git a/src/PropertyEditor.h b/src/PropertyEditor.h index cbb579d..adce4e0 100644 --- a/src/PropertyEditor.h +++ b/src/PropertyEditor.h @@ -1,7 +1,7 @@ #ifndef PROPERTYEDITOR_H #define PROPERTYEDITOR_H -#include "Property.h" +#include "PropertyContext.h" #include @@ -15,8 +15,6 @@ class PropertyEditor; namespace internal { - class PropertyContextPrivate; - template constexpr bool isPropertyEditor() { @@ -29,57 +27,6 @@ namespace internal } } // namespace internal -class PropertyContext -{ - friend class PM::internal::PropertyContextPrivate; - -public: - Property property() const; - QVariant value() const; - - bool isValid() const; - - template - T object() const // WARNING: this function would never perform any checks for type-safety - { - return getObjectValue(std::is_pointer()); - } - - PropertyGrid *propertyGrid() const; - -private: // TODO: wouldn't this cause problems when writing unit tests for user property editors? - PropertyContext(); - PropertyContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid); - - // function for pointer types - template - T getObjectValue(std::true_type) const - { - return static_cast(m_object); - } - - // function for non-pointer types - template - T getObjectValue(std::false_type) const - { - T *result = static_cast(m_object); - if (result == nullptr) - return T(); - - return *result; - } - -private: - Property m_property; - QVariant m_value; - void *m_object; // TODO: use std::any - QPointer m_propertyGrid; - - // Meta values - bool m_isValid; - std::function m_valueChangedSlot; -}; - class PropertyEditor { public: diff --git a/src/PropertyGrid.cpp b/src/PropertyGrid.cpp index 9ccd113..052ba03 100644 --- a/src/PropertyGrid.cpp +++ b/src/PropertyGrid.cpp @@ -1,6 +1,7 @@ #include "PropertyGrid.h" #include "PropertyGrid_p.h" +#include "PropertyContext_p.h" #include "PropertyGridTreeItem.h" #include "PropertyGridTreeModel.h" #include "QtCompat_p.h" @@ -357,69 +358,6 @@ QString internal::PropertyEditorWidget::valueToString(const QVariant &value) con return propertyEditor()->toString(newContext); } -PropertyContext &internal::PropertyContextPrivate::invalidContext() -{ - static PropertyContext result; - - return result; -} - -PropertyContext internal::PropertyContextPrivate::createContext(const Property &property) -{ - return PropertyContext(property, QVariant(), nullptr, nullptr); -} - -PropertyContext internal::PropertyContextPrivate::createContext(const QVariant &value, bool valid) -{ - PropertyContext result; - result.m_value = value; - result.m_isValid = valid; - - return result; -} - -PropertyContext internal::PropertyContextPrivate::createContext(const PropertyContext &other, const QVariant &newValue) -{ - PropertyContext result = other; - result.m_value = newValue; - - return result; -} - -PropertyContext internal::PropertyContextPrivate::createContext(const Property &property, const QVariant &value, void *object, - PropertyGrid *propertyGrid) -{ - return PropertyContext(property, value, object, propertyGrid); -} - -void internal::PropertyContextPrivate::setValue(PropertyContext &context, const QVariant &value) -{ - context.m_value = value; - notifyValueChanged(context, value); -} - -internal::PropertyContextPrivate::valueChangedSlot_t internal::PropertyContextPrivate::defaultValueChangedSlot() -{ - static valueChangedSlot_t result = [](const QVariant &) {}; - - return result; -} - -void internal::PropertyContextPrivate::disconnectValueChangedSlot(PropertyContext &context) -{ - context.m_valueChangedSlot = defaultValueChangedSlot(); -} - -void internal::PropertyContextPrivate::connectValueChangedSlot(PropertyContext &context, const valueChangedSlot_t &slot) -{ - context.m_valueChangedSlot = slot; -} - -void internal::PropertyContextPrivate::notifyValueChanged(const PropertyContext &context, const QVariant &newValue) -{ - context.m_valueChangedSlot(newValue); -} - QString internal::ModelIndexHelperFunctions::firstValueInRowAsString(const QModelIndex &index, Qt::ItemDataRole role) { QVariant result = firstValueInRow(index, role); @@ -587,7 +525,7 @@ void PropertyGridPrivate::updatePropertyValue(const QModelIndex &index, const QV QModelIndex valueIndex = PM::internal::siblingAtColumn(index, 1); - internal::PropertyContextPrivate::setValue(context, value); + PropertyContextPrivate::setValue(context, value); m_model.setData(valueIndex, value, Qt::EditRole); m_model.setData(valueIndex, editor->toString(context), Qt::DisplayRole); @@ -719,7 +657,7 @@ void PropertyGrid::addProperty(const Property &property, const QVariant &value, return; } - PropertyContext context = internal::PropertyContextPrivate::createContext(property, value, object, this); + PropertyContext context = PropertyContextPrivate::createContext(property, value, object, this); QModelIndex propertyIndex = d->m_model.addProperty(context); @@ -765,7 +703,7 @@ PropertyContext PropertyGrid::getPropertyContext(const QString &propertyName) co if (treeItem != nullptr) return treeItem->context; - return internal::PropertyContextPrivate::invalidContext(); + return PropertyContextPrivate::invalidContext(); } void PropertyGrid::clearProperties() diff --git a/src/PropertyGridTreeItem.cpp b/src/PropertyGridTreeItem.cpp index 21452a1..574e9e8 100644 --- a/src/PropertyGridTreeItem.cpp +++ b/src/PropertyGridTreeItem.cpp @@ -1,5 +1,6 @@ #include "PropertyGridTreeItem.h" -#include "PropertyGrid_p.h" + +#include "PropertyContext_p.h" using namespace PM; @@ -178,7 +179,7 @@ internal::PropertyGridTreeItem *internal::PropertyGridTreeItem::getChild(size_t internal::PropertyGridTreeItem *internal::PropertyGridTreeItem::addChild(const PropertyContext &context) { - if (!insertChildren(children.size(), 1, 2)) + if (!insertChildren(static_cast(children.size()), 1, 2)) return nullptr; auto &newChild = children.back(); @@ -269,10 +270,7 @@ int internal::PropertyGridTreeItem::indexInParent(bool showTransientItems) const return -1; } -internal::PropertyGridTreeItem::PropertyGridTreeItem() : - context(PM::internal::PropertyContextPrivate::invalidContext()), - parent(nullptr), - isTransient(false) +internal::PropertyGridTreeItem::PropertyGridTreeItem() : context(PM::PropertyContextPrivate::invalidContext()), parent(nullptr), isTransient(false) { } diff --git a/src/PropertyGridTreeItem.h b/src/PropertyGridTreeItem.h index 01df53a..48045ac 100644 --- a/src/PropertyGridTreeItem.h +++ b/src/PropertyGridTreeItem.h @@ -1,7 +1,7 @@ #ifndef PROPERTYGRIDTREEITEM_H #define PROPERTYGRIDTREEITEM_H -#include "PropertyEditor.h" +#include "PropertyContext.h" #include #include diff --git a/src/PropertyGridTreeModel.cpp b/src/PropertyGridTreeModel.cpp index a5f0cd1..e4958ae 100644 --- a/src/PropertyGridTreeModel.cpp +++ b/src/PropertyGridTreeModel.cpp @@ -1,6 +1,8 @@ #include "PropertyGridTreeModel.h" + +#include "PropertyContext_p.h" #include "PropertyGridTreeItem.h" -#include "PropertyGrid_p.h" +#include "QtCompat_p.h" #include #include @@ -194,7 +196,7 @@ internal::PropertyGridTreeItem *internal::PropertyGridTreeModel::getCategoryItem if (m_categoriesMap.contains(category)) return m_categoriesMap.value(category); - const PropertyContext tempCategoryContext = internal::PropertyContextPrivate::createContext(PM::Property(category, QMetaType::UnknownType)); + const PropertyContext tempCategoryContext = PropertyContextPrivate::createContext(PM::Property(category, QMetaType::UnknownType)); PropertyGridTreeItem *result = m_rootItem->addChild(tempCategoryContext); result->isTransient = true; // TODO: make category item expanded by default?!! diff --git a/src/PropertyGridTreeModel.h b/src/PropertyGridTreeModel.h index e0083d6..bf47a21 100644 --- a/src/PropertyGridTreeModel.h +++ b/src/PropertyGridTreeModel.h @@ -12,7 +12,7 @@ class PropertyGrid; namespace internal { - class PropertyGridTreeItem; + struct PropertyGridTreeItem; class PropertyGridTreeModel : public QAbstractItemModel { diff --git a/src/PropertyGrid_p.h b/src/PropertyGrid_p.h index c4f0e63..954b62c 100644 --- a/src/PropertyGrid_p.h +++ b/src/PropertyGrid_p.h @@ -112,30 +112,6 @@ namespace internal PropertyEditorLineEdit m_propertylineEdit; }; - // FIXME: move to a more appropriate location - class PropertyContextPrivate - { - public: - using valueChangedSlot_t = std::function; - - public: - static PropertyContext &invalidContext(); - - static PropertyContext createContext(const Property &property); - static PropertyContext createContext(const QVariant &value, bool valid = true); - static PropertyContext createContext(const PropertyContext &other, const QVariant &newValue); - static PropertyContext createContext(const Property &property, const QVariant &value, void *object, PropertyGrid *propertyGrid); - - static void setValue(PropertyContext &context, const QVariant &value); - - static valueChangedSlot_t defaultValueChangedSlot(); - - static void disconnectValueChangedSlot(PropertyContext &context); - static void connectValueChangedSlot(PropertyContext &context, const valueChangedSlot_t &slot); - - static void notifyValueChanged(const PropertyContext &context, const QVariant &newValue); - }; - // TODO: can we get rid of this altogether?!! struct ModelIndexHelperFunctions { From cb06e3fd7b20b2bd633e8841fbdccc964a81ac15 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:55:00 +0300 Subject: [PATCH 05/18] PM-140 Add replacePropertyEditor API to PropertyGrid Introduces a new replacePropertyEditor method to allow replacing an existing property editor with a new one. The implementation ensures the old editor is removed, the new one is added, and property entries are updated accordingly. The previous removePropertyEditor method and its implementation have been removed. --- src/PropertyGrid.cpp | 27 ++++++++++++++++++++------- src/PropertyGrid.h | 18 ++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/PropertyGrid.cpp b/src/PropertyGrid.cpp index 052ba03..25a41c4 100644 --- a/src/PropertyGrid.cpp +++ b/src/PropertyGrid.cpp @@ -716,10 +716,28 @@ QStringList PropertyGrid::propertyNames() const return d->m_model.getPropertiesNames(); } -void PropertyGrid::removePropertyEditor_impl(TypeId typeId) +void PropertyGrid::replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId newEditorTypeId, std::unique_ptr &&editor) { - d->m_propertyEditors.erase(typeId); + if (d->m_propertyEditors.find(oldEditorTypeId) == d->m_propertyEditors.end()) + { + qWarning() << "Cannot replace a non-existing editor"; + return; + } + d->m_propertyEditors.erase(oldEditorTypeId); + + // no need to add a new instance of the default property editor if the user specified it explicitly + if (newEditorTypeId == internal::getTypeId()) + return; + + addPropertyEditor_impl(newEditorTypeId, std::move(editor)); +} + +void PropertyGrid::addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor) +{ + d->m_propertyEditors.emplace(typeId, std::move(editor)); + + // Force all property entries in the view to get calculated using the updated editors list const QStringList names = propertyNames(); for (const QString &propertyName : names) { @@ -730,8 +748,3 @@ void PropertyGrid::removePropertyEditor_impl(TypeId typeId) d->updatePropertyValue(propertyIndex, context.value()); } } - -void PropertyGrid::addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor) -{ - d->m_propertyEditors.emplace(typeId, std::move(editor)); -} diff --git a/src/PropertyGrid.h b/src/PropertyGrid.h index 8ab177a..f30363e 100644 --- a/src/PropertyGrid.h +++ b/src/PropertyGrid.h @@ -46,8 +46,13 @@ class PropertyGrid : public QWidget public: /* EXPERIMENTAL API */ /**/ - template ()>> - void removePropertyEditor(); + template ()>, + typename = internal::templateCheck_t()> + // clang-format on + > + void replacePropertyEditor(); void clearProperties(); // Requested: 2 QStringList propertyNames() const; // Requested: 2 @@ -56,7 +61,8 @@ class PropertyGrid : public QWidget void propertyValueChanged(const PM::PropertyContext &context); private: // stable internal functions - void removePropertyEditor_impl(TypeId typeId); + void replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId newEditorTypeId, std::unique_ptr &&editor); + void addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor); private: @@ -73,10 +79,10 @@ inline void PM::PropertyGrid::addProperty(const QString &name, const QVariant &v addProperty(Property(name, internal::getVariantTypeId(value), attributes...), value); } -template -inline void PM::PropertyGrid::removePropertyEditor() +template +inline void PM::PropertyGrid::replacePropertyEditor() { - removePropertyEditor_impl(internal::getTypeId()); + replacePropertyEditor_impl(internal::getTypeId(), internal::getTypeId(), std::make_unique()); } template From 5b30dddc55200568149ff3a783de7561c7c9ccb6 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:18:22 +0300 Subject: [PATCH 06/18] PM-140 Refactor property editor registration and usage Property editors are now registered by default and managed via shared_ptr instead of unique_ptr. Manual registration in examples and README has been removed, and internal APIs updated to use shared_ptr for property editors. This simplifies usage and improves memory management. --- README.md | 8 -------- examples/property_grid_showcase/main.cpp | 7 ------- src/PropertyEditor.cpp | 25 ++++++++++++++++++++++++ src/PropertyEditor.h | 3 +++ src/PropertyGrid.cpp | 14 ++++++++++--- src/PropertyGrid.h | 4 ++-- src/PropertyGrid_p.h | 2 +- 7 files changed, 42 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 70978f3..66826cf 100644 --- a/README.md +++ b/README.md @@ -71,14 +71,6 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); PM::PropertyGrid propertyGrid; - // - // ONLY FOR NOW... - // - // Add editors for the property types that you're using - // This won't be required in the future - // - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); // Add simple properties propertyGrid.addProperty("Name", QString("My Object")); diff --git a/examples/property_grid_showcase/main.cpp b/examples/property_grid_showcase/main.cpp index 6bc57de..b985f14 100644 --- a/examples/property_grid_showcase/main.cpp +++ b/examples/property_grid_showcase/main.cpp @@ -46,13 +46,6 @@ int main(int argc, char **argv) QApplication app(argc, argv); PM::PropertyGrid propertyGrid; - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); - propertyGrid.addPropertyEditor(); // add properties addProperty(propertyGrid, "Property1", "Value1", "Description of property1"); diff --git a/src/PropertyEditor.cpp b/src/PropertyEditor.cpp index 82095ce..f9fddb1 100644 --- a/src/PropertyEditor.cpp +++ b/src/PropertyEditor.cpp @@ -39,6 +39,31 @@ const QString &boolToString(bool value) return value ? trueKey : falseKey; } +template +std::pair> createPropertyEditorMapEntry() +{ + return std::pair>(internal::getTypeId(), std::make_shared()); +} + +const PropertyEditorsMap_t &PM::internal::defaultPropertyEditors() +{ + // TODO: should have the ability to modify this list at runtime if we ever made this API public + + // clang-format off + static PropertyEditorsMap_t instance = { + createPropertyEditorMapEntry(), + createPropertyEditorMapEntry(), + createPropertyEditorMapEntry(), + createPropertyEditorMapEntry(), + createPropertyEditorMapEntry(), + createPropertyEditorMapEntry(), + createPropertyEditorMapEntry(), + }; + // clang-format on + + return instance; +} + bool PropertyEditor::canHandle(const PropertyContext &context) const { return true; diff --git a/src/PropertyEditor.h b/src/PropertyEditor.h index adce4e0..877cef2 100644 --- a/src/PropertyEditor.h +++ b/src/PropertyEditor.h @@ -25,6 +25,9 @@ namespace internal return isPropertyEditor; } + + using PropertyEditorsMap_t = std::unordered_map>; + const PropertyEditorsMap_t &defaultPropertyEditors(); } // namespace internal class PropertyEditor diff --git a/src/PropertyGrid.cpp b/src/PropertyGrid.cpp index 25a41c4..88de198 100644 --- a/src/PropertyGrid.cpp +++ b/src/PropertyGrid.cpp @@ -484,7 +484,11 @@ QSize internal::PropertyGridItemDelegate::sizeHint(const QStyleOptionViewItem &o return result; } -PropertyGridPrivate::PropertyGridPrivate(PropertyGrid *q) : q(q), ui(new Ui::PropertyGrid()), tableViewItemDelegate(q) +PropertyGridPrivate::PropertyGridPrivate(PropertyGrid *q) : + q(q), + ui(new Ui::PropertyGrid()), + tableViewItemDelegate(q), + m_propertyEditors(internal::defaultPropertyEditors()) { } @@ -716,7 +720,7 @@ QStringList PropertyGrid::propertyNames() const return d->m_model.getPropertiesNames(); } -void PropertyGrid::replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId newEditorTypeId, std::unique_ptr &&editor) +void PropertyGrid::replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId newEditorTypeId, std::shared_ptr &&editor) { if (d->m_propertyEditors.find(oldEditorTypeId) == d->m_propertyEditors.end()) { @@ -733,8 +737,12 @@ void PropertyGrid::replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId new addPropertyEditor_impl(newEditorTypeId, std::move(editor)); } -void PropertyGrid::addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor) +void PropertyGrid::addPropertyEditor_impl(TypeId typeId, std::shared_ptr &&editor) { + // If the new editor already exists then there is no need to add it again + if (d->m_propertyEditors.find(typeId) != d->m_propertyEditors.end()) + return; + d->m_propertyEditors.emplace(typeId, std::move(editor)); // Force all property entries in the view to get calculated using the updated editors list diff --git a/src/PropertyGrid.h b/src/PropertyGrid.h index f30363e..9b6f6f7 100644 --- a/src/PropertyGrid.h +++ b/src/PropertyGrid.h @@ -61,9 +61,9 @@ class PropertyGrid : public QWidget void propertyValueChanged(const PM::PropertyContext &context); private: // stable internal functions - void replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId newEditorTypeId, std::unique_ptr &&editor); + void replacePropertyEditor_impl(TypeId oldEditorTypeId, TypeId newEditorTypeId, std::shared_ptr &&editor); - void addPropertyEditor_impl(TypeId typeId, std::unique_ptr &&editor); + void addPropertyEditor_impl(TypeId typeId, std::shared_ptr &&editor); private: PropertyGridPrivate *d; diff --git a/src/PropertyGrid_p.h b/src/PropertyGrid_p.h index 954b62c..7074447 100644 --- a/src/PropertyGrid_p.h +++ b/src/PropertyGrid_p.h @@ -183,7 +183,7 @@ class PropertyGridPrivate internal::PropertyGridItemDelegate tableViewItemDelegate; internal::PropertyGridTreeModel m_model; - std::unordered_map> m_propertyEditors; + internal::PropertyEditorsMap_t m_propertyEditors; }; } // namespace PM From 3a3d9d54b9a719cb816bc030f7ae0a43a2f26cb6 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:22:20 +0300 Subject: [PATCH 07/18] PM-140 Rename internal headers to *_p.h and update includes Renamed PropertyGridTreeItem.h and PropertyGridTreeModel.h to PropertyGridTreeItem_p.h and PropertyGridTreeModel_p.h to clarify their internal status. Updated all relevant #include directives and CMakeLists.txt entries. Added internal-use warning comments to affected headers for better API boundary documentation. --- src/CMakeLists.txt | 4 ++-- src/PropertyContext_p.h | 10 ++++++++++ src/PropertyGrid.cpp | 4 ++-- src/PropertyGridTreeItem.cpp | 2 +- ...tyGridTreeItem.h => PropertyGridTreeItem_p.h} | 16 +++++++++++++--- src/PropertyGridTreeModel.cpp | 4 ++-- ...GridTreeModel.h => PropertyGridTreeModel_p.h} | 16 +++++++++++++--- src/PropertyGrid_p.h | 12 +++++++++++- 8 files changed, 54 insertions(+), 14 deletions(-) rename src/{PropertyGridTreeItem.h => PropertyGridTreeItem_p.h} (89%) rename src/{PropertyGridTreeModel.h => PropertyGridTreeModel_p.h} (87%) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d5af314..78b9329 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,9 +24,9 @@ add_library(PmPropertyGrid STATIC PropertyGrid.ui PropertyGrid_p.h PropertyGrid.cpp - PropertyGridTreeItem.h + PropertyGridTreeItem_p.h PropertyGridTreeItem.cpp - PropertyGridTreeModel.h + PropertyGridTreeModel_p.h PropertyGridTreeModel.cpp PropertyContext_p.h PropertyContext.cpp diff --git a/src/PropertyContext_p.h b/src/PropertyContext_p.h index 1b4952b..0b596d5 100644 --- a/src/PropertyContext_p.h +++ b/src/PropertyContext_p.h @@ -1,6 +1,16 @@ #ifndef PROPERTYCONTEXT_P_H #define PROPERTYCONTEXT_P_H +// +// W A R N I N G +// ------------- +// +// This file is not part of the PM::PropertyGrid API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// + #include "PropertyContext.h" namespace PM diff --git a/src/PropertyGrid.cpp b/src/PropertyGrid.cpp index 88de198..6e72d86 100644 --- a/src/PropertyGrid.cpp +++ b/src/PropertyGrid.cpp @@ -2,8 +2,8 @@ #include "PropertyGrid_p.h" #include "PropertyContext_p.h" -#include "PropertyGridTreeItem.h" -#include "PropertyGridTreeModel.h" +#include "PropertyGridTreeItem_p.h" +#include "PropertyGridTreeModel_p.h" #include "QtCompat_p.h" #include diff --git a/src/PropertyGridTreeItem.cpp b/src/PropertyGridTreeItem.cpp index 574e9e8..2d5dcaf 100644 --- a/src/PropertyGridTreeItem.cpp +++ b/src/PropertyGridTreeItem.cpp @@ -1,4 +1,4 @@ -#include "PropertyGridTreeItem.h" +#include "PropertyGridTreeItem_p.h" #include "PropertyContext_p.h" diff --git a/src/PropertyGridTreeItem.h b/src/PropertyGridTreeItem_p.h similarity index 89% rename from src/PropertyGridTreeItem.h rename to src/PropertyGridTreeItem_p.h index 48045ac..f543be7 100644 --- a/src/PropertyGridTreeItem.h +++ b/src/PropertyGridTreeItem_p.h @@ -1,5 +1,15 @@ -#ifndef PROPERTYGRIDTREEITEM_H -#define PROPERTYGRIDTREEITEM_H +#ifndef PROPERTYGRIDTREEITEM_P_H +#define PROPERTYGRIDTREEITEM_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the PM::PropertyGrid API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// #include "PropertyContext.h" @@ -106,4 +116,4 @@ namespace internal } // namespace internal } // namespace PM -#endif // PROPERTYGRIDTREEITEM_H +#endif // PROPERTYGRIDTREEITEM_P_H diff --git a/src/PropertyGridTreeModel.cpp b/src/PropertyGridTreeModel.cpp index e4958ae..26fdbad 100644 --- a/src/PropertyGridTreeModel.cpp +++ b/src/PropertyGridTreeModel.cpp @@ -1,7 +1,7 @@ -#include "PropertyGridTreeModel.h" +#include "PropertyGridTreeModel_p.h" #include "PropertyContext_p.h" -#include "PropertyGridTreeItem.h" +#include "PropertyGridTreeItem_p.h" #include "QtCompat_p.h" #include diff --git a/src/PropertyGridTreeModel.h b/src/PropertyGridTreeModel_p.h similarity index 87% rename from src/PropertyGridTreeModel.h rename to src/PropertyGridTreeModel_p.h index bf47a21..a2a73f0 100644 --- a/src/PropertyGridTreeModel.h +++ b/src/PropertyGridTreeModel_p.h @@ -1,5 +1,15 @@ -#ifndef PROPERTYGRIDTREEMODEL_H -#define PROPERTYGRIDTREEMODEL_H +#ifndef PROPERTYGRIDTREEMODEL_P_H +#define PROPERTYGRIDTREEMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the PM::PropertyGrid API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// #include "PropertyEditor.h" @@ -76,4 +86,4 @@ inline QStringList PM::internal::PropertyGridTreeModel::getPropertiesNames() con return m_propertiesMap.keys(); } -#endif // PROPERTYGRIDTREEMODEL_H +#endif // PROPERTYGRIDTREEMODEL_P_H diff --git a/src/PropertyGrid_p.h b/src/PropertyGrid_p.h index 7074447..af96294 100644 --- a/src/PropertyGrid_p.h +++ b/src/PropertyGrid_p.h @@ -1,10 +1,20 @@ #ifndef PROPERTYGRID_P_H #define PROPERTYGRID_P_H +// +// W A R N I N G +// ------------- +// +// This file is not part of the PM::PropertyGrid API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// + #include "PropertyGrid.h" #include "ui_PropertyGrid.h" -#include "PropertyGridTreeModel.h" +#include "PropertyGridTreeModel_p.h" #include #include From 3b89fc04e35179595da32c7326c1f0ea5b37cbc0 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 04:42:28 +0300 Subject: [PATCH 08/18] PM-140Add Qt version-compatible string helpers Introduced stringLeft and stringMid helper functions in QtCompat_p.h to handle QString slicing across different Qt versions. Updated FontPropertyEditor to use these helpers for improved compatibility and code clarity. --- src/PropertyEditor.cpp | 12 ++++++------ src/QtCompat_p.h | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/PropertyEditor.cpp b/src/PropertyEditor.cpp index f9fddb1..1dde82a 100644 --- a/src/PropertyEditor.cpp +++ b/src/PropertyEditor.cpp @@ -465,7 +465,7 @@ QVariant FontPropertyEditor::fromString(const QString &value, const PropertyCont QString sizeValue = properties[1]; bool isPixelSize = sizeValue.endsWith("px"); - int fontSize = sizeValue.left(sizeValue.length() - (isPixelSize ? 2 : 0)).toInt(); + int fontSize = PM::internal::stringLeft(sizeValue, sizeValue.length() - (isPixelSize ? 2 : 0)).toInt(); QFont font(fontFamily); if (isPixelSize) @@ -479,15 +479,15 @@ QVariant FontPropertyEditor::fromString(const QString &value, const PropertyCont const QString &prop = properties[i]; if (prop.startsWith(weightKey)) - font.setWeight(static_cast(prop.mid(weightKey.length()).toInt())); + font.setWeight(static_cast(PM::internal::stringMid(prop, weightKey.length()).toInt())); else if (prop.startsWith(styleKey)) - font.setStyle(static_cast(prop.mid(styleKey.length()).toInt())); + font.setStyle(static_cast(PM::internal::stringMid(prop, styleKey.length()).toInt())); else if (prop.startsWith(underlineKey)) - font.setUnderline(prop.mid(underlineKey.length()).toInt()); + font.setUnderline(PM::internal::stringMid(prop, underlineKey.length()).toInt()); else if (prop.startsWith(strikeOutKey)) - font.setStrikeOut(prop.mid(strikeOutKey.length()).toInt()); + font.setStrikeOut(PM::internal::stringMid(prop, strikeOutKey.length()).toInt()); else if (prop.startsWith(kerningKey)) - font.setKerning(prop.mid(kerningKey.length()).toInt()); + font.setKerning(PM::internal::stringMid(prop, kerningKey.length()).toInt()); } return QVariant::fromValue(font); diff --git a/src/QtCompat_p.h b/src/QtCompat_p.h index d56ffc3..5e99234 100644 --- a/src/QtCompat_p.h +++ b/src/QtCompat_p.h @@ -17,6 +17,14 @@ namespace PM { namespace internal { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + using StringRef_t = QStringView; +#elif QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + using StringRef_t = QStringRef; +#else + using StringRef_t = QString; +#endif + inline QModelIndex siblingAtColumn(const QModelIndex &index, int column) { #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) @@ -56,6 +64,28 @@ namespace internal #endif } + inline StringRef_t stringLeft(const QString &input, int n) + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return QStringView(input).left(n); +#elif QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + return input.leftRef(n); +#else + return input.left(n); +#endif + } + + inline StringRef_t stringMid(const QString &input, int position, int n = -1) + { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + return QStringView(input).mid(position, n); +#elif QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + return input.midRef(position, n); +#else + return input.mid(position, n); +#endif + } + // TODO: is this function really needed?!! inline QVariant createDefaultVariantForType(int type) { From bafea6569d84b46057ef3f369927fa7524066e44 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 05:27:13 +0300 Subject: [PATCH 09/18] PM-140 Ignore markdown file changes in CI workflows Updated Linux, macOS, and Windows GitHub Actions workflows to ignore pushes and pull requests that only modify .md files. This reduces unnecessary CI runs triggered by documentation changes. --- .github/workflows/linux.yml | 5 +++++ .github/workflows/macos.yml | 5 +++++ .github/workflows/windows.yml | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2331bc9..0900949 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -4,8 +4,13 @@ on: workflow_dispatch: push: branches: [main, develop] + paths-ignore: + - '*.md' + pull_request: branches: [main, develop] + paths-ignore: + - '*.md' jobs: get-matrix: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3766cf8..87c474c 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -4,8 +4,13 @@ on: workflow_dispatch: push: branches: [main, develop] + paths-ignore: + - '*.md' + pull_request: branches: [main, develop] + paths-ignore: + - '*.md' jobs: get-matrix: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index d4203cd..bab5d35 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,8 +4,13 @@ on: workflow_dispatch: push: branches: [main, develop] + paths-ignore: + - '*.md' + pull_request: branches: [main, develop] + paths-ignore: + - '*.md' jobs: get-matrix: From 1c7f1e6c9c4ed53c55b522d80e47d4e2184f7598 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 05:43:25 +0300 Subject: [PATCH 10/18] PM-140 fix build errors for Qt <= 5.12 --- src/PropertyGridTreeItem_p.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PropertyGridTreeItem_p.h b/src/PropertyGridTreeItem_p.h index f543be7..be761d0 100644 --- a/src/PropertyGridTreeItem_p.h +++ b/src/PropertyGridTreeItem_p.h @@ -15,6 +15,7 @@ #include #include +#include class TreeItem { From ffbe7ca63815ca08d0d949104c101fff0eb1becd Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 06:08:00 +0300 Subject: [PATCH 11/18] Revise README for current status and features Updated the README to reflect the current status and planned features for Qt type support. --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 66826cf..19254ec 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,20 @@ This library provides a flexible and customizable property editor that displays > **Current Status:** > - ✅ **API is stable** - Safe to use in your projects without worrying about breaking changes > - ⚠️ **Implementation is not fully polished** - Some features may have rough edges -> - 🚧 **Work in progress** - Actively being improved in my free time to become production-ready +> - 🚧 **Work in progress** - Actively being improved to become production-ready > > Feel free to use it, report issues, and contribute! The core functionality works well, and the API won't change in ways that break your code. ## Features -- **Comprehensive Type Support**: Built-in support for all major Qt types including: - - Basic types: `int`, `bool`, `QString`, `double` - - Geometric types: `QPoint`, `QPointF`, `QSize`, `QSizeF`, `QRect`, `QRectF`, `QLine`, `QLineF` - - Graphics types: `QColor`, `QFont`, `QIcon`, `QPixmap`, `QBitmap`, `QImage` - - Advanced types: `QVector2D`, `QVector3D`, `QVector4D`, `QMatrix4x4`, `QPolygon`, `QPolygonF` - - Date/Time types: `QDate`, `QTime`, `QDateTime` - - UI types: `QKeySequence`, `QCursor`, `QChar` - - Object types: `QObject*` and custom enums +- **Comprehensive Type Support**: Planned Built-in support for all major Qt types including: + - [x] **Basic types**: `int`, `bool`, `QString`, `double` + - [ ] **Geometric types**: `QPoint`, `QPointF`, `QSize`, `QSizeF`, `QRect`, `QRectF`, `QLine`, `QLineF` + - [x] **Graphics types**: `QColor`, `QFont`, `QIcon`, `QPixmap`, `QBitmap`, `QImage` + - [ ] **Advanced types**: `QVector2D`, `QVector3D`, `QVector4D`, `QMatrix4x4`, `QPolygon`, `QPolygonF` + - [x] **Date/Time types**: `QDate`, `QTime`, `QDateTime` + - [ ] **UI types** (partial): `QKeySequence`, `QCursor`, `QChar` + - [ ] **Object types**: `QObject*` and custom enums - **Property Attributes**: Rich attribute system for enhanced property configuration: - `DescriptionAttribute`: Add helpful descriptions to properties From dbf5ca842169fcaed3ba69363d76f457ab15ee78 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 06:27:23 +0300 Subject: [PATCH 12/18] Add option to control building of example projects Introduces the PM_BUILD_PROPERTY_GRID_EXAMPLES CMake option to enable or disable building example projects. Updates documentation to show how to disable examples when integrating as a subdirectory. Also moves include_directories to target_include_directories for better CMake practices. --- CMakeLists.txt | 7 ++++++- README.md | 4 +++- examples/CMakeLists.txt | 2 -- src/CMakeLists.txt | 5 +++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 11e1036..88b2aca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,5 +10,10 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(PM_BUILD_PROPERTY_GRID_EXAMPLES "Determines whether or not the example projects should be built with the library" ON) + add_subdirectory(src) -add_subdirectory(examples) + +if(PM_BUILD_PROPERTY_GRID_EXAMPLES) + add_subdirectory(examples) +endif() diff --git a/README.md b/README.md index 19254ec..a416cb8 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,8 @@ cmake --build . You can easily integrate PmPropertyGrid into your CMake project using `add_subdirectory`: ```cmake -# Add PmPropertyGrid as a subdirectory +# Add PmPropertyGrid as a subdirectory and disable examples +set(PM_BUILD_PROPERTY_GRID_EXAMPLES OFF) add_subdirectory(path/to/PmPropertyGrid) # Link against your target @@ -160,6 +161,7 @@ find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) # Add PmPropertyGrid +set(PM_BUILD_PROPERTY_GRID_EXAMPLES OFF) add_subdirectory(third_party/PmPropertyGrid) # Create your executable diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 80c261b..f65393d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1 @@ -include_directories(${CMAKE_SOURCE_DIR}/src) - add_subdirectory(property_grid_showcase) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 78b9329..d94f164 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -36,6 +36,11 @@ add_library(PmPropertyGrid STATIC add_library(PM::PropertyGrid ALIAS PmPropertyGrid) +target_include_directories(PmPropertyGrid + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + target_link_libraries(PmPropertyGrid # using versionless aliasses like Qt::Core doesn't work prior to Qt5.15 Qt${QT_VERSION_MAJOR}::Core From 527bf4229935319deb71bf8aa5aedfdc3a1457e6 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Mon, 20 Oct 2025 06:32:08 +0300 Subject: [PATCH 13/18] Update README with PropertyContext file details Added PropertyContext files to the project structure. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a416cb8..8003260 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ PmPropertyGrid/ ├── src/ # Core library source code │ ├── PropertyGrid.h/cpp # Main property grid widget │ ├── Property.h/cpp # Property definition and attributes +│ ├── PropertyContext.h/cpp # Property definition, value and associated meta info │ └── PropertyEditor.h/cpp # Property editor framework ├── examples/ # Example applications │ └── property_grid_showcase/ # Comprehensive demo From 975cd62977a470718533b5b884325d4170783936 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:44:10 +0200 Subject: [PATCH 14/18] PM-183 initial implementation for object_property_grid --- examples/CMakeLists.txt | 1 + examples/object_property_grid/CMakeLists.txt | 19 ++++ examples/object_property_grid/Car.cpp | 47 ++++++++++ examples/object_property_grid/Car.h | 36 ++++++++ .../ObjectPropertyGrid.cpp | 91 +++++++++++++++++++ .../object_property_grid/ObjectPropertyGrid.h | 25 +++++ examples/object_property_grid/main.cpp | 31 +++++++ .../property_grid_showcase/CMakeLists.txt | 3 +- 8 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 examples/object_property_grid/CMakeLists.txt create mode 100644 examples/object_property_grid/Car.cpp create mode 100644 examples/object_property_grid/Car.h create mode 100644 examples/object_property_grid/ObjectPropertyGrid.cpp create mode 100644 examples/object_property_grid/ObjectPropertyGrid.h create mode 100644 examples/object_property_grid/main.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f65393d..a36ed11 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(property_grid_showcase) +add_subdirectory(object_property_grid) diff --git a/examples/object_property_grid/CMakeLists.txt b/examples/object_property_grid/CMakeLists.txt new file mode 100644 index 0000000..efedd4c --- /dev/null +++ b/examples/object_property_grid/CMakeLists.txt @@ -0,0 +1,19 @@ +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +find_package(QT NAMES Qt6 Qt5 COMPONENTS Core Widgets REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Widgets REQUIRED) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core) + +add_executable(object_property_grid + main.cpp + ObjectPropertyGrid.h ObjectPropertyGrid.cpp + Car.h Car.cpp +) + +target_link_libraries(object_property_grid + PRIVATE + PM::PropertyGrid +) +target_link_libraries(object_property_grid PRIVATE Qt${QT_VERSION_MAJOR}::Core) diff --git a/examples/object_property_grid/Car.cpp b/examples/object_property_grid/Car.cpp new file mode 100644 index 0000000..f67d53e --- /dev/null +++ b/examples/object_property_grid/Car.cpp @@ -0,0 +1,47 @@ +#include "Car.h" + +Car::Car(QObject *parent) : QObject(parent), m_electric(false), m_year(2025) +{ +} + +QString Car::brand() const +{ + return m_brand; +} + +void Car::setBrand(const QString &b) +{ + if (m_brand == b) + return; + + m_brand = b; + emit brandChanged(); +} + +int Car::year() const +{ + return m_year; +} + +void Car::setYear(int y) +{ + if (m_year == y) + return; + + m_year = y; + emit yearChanged(); +} + +bool Car::electric() const +{ + return m_electric; +} + +void Car::setElectric(bool e) +{ + if (m_electric != e) + return; + + m_electric = e; + emit electricChanged(); +} diff --git a/examples/object_property_grid/Car.h b/examples/object_property_grid/Car.h new file mode 100644 index 0000000..989a490 --- /dev/null +++ b/examples/object_property_grid/Car.h @@ -0,0 +1,36 @@ +#ifndef CAR_H +#define CAR_H + +#include + +class Car : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString brand READ brand WRITE setBrand NOTIFY brandChanged) + Q_PROPERTY(int year READ year WRITE setYear NOTIFY yearChanged) + Q_PROPERTY(bool electric READ electric WRITE setElectric NOTIFY electricChanged) + +public: + Car(QObject *parent = nullptr); + + QString brand() const; + void setBrand(const QString &b); + + int year() const; + void setYear(int y); + + bool electric() const; + void setElectric(bool e); + +signals: + void brandChanged(); + void yearChanged(); + void electricChanged(); + +private: + QString m_brand; + bool m_electric; + int m_year; +}; + +#endif // CAR_H diff --git a/examples/object_property_grid/ObjectPropertyGrid.cpp b/examples/object_property_grid/ObjectPropertyGrid.cpp new file mode 100644 index 0000000..9e3f0fa --- /dev/null +++ b/examples/object_property_grid/ObjectPropertyGrid.cpp @@ -0,0 +1,91 @@ +#include "ObjectPropertyGrid.h" + +#include + +ObjectPropertyGrid::ObjectPropertyGrid(QWidget *parent) : PM::PropertyGrid(parent) +{ +} + +void ObjectPropertyGrid::setSelectedObject(QObject *object) +{ + m_objects.clear(); + + if (object != nullptr) + m_objects.append(object); + + updateProperties(); +} + +void ObjectPropertyGrid::setSelectedObjects(const QList &objects) +{ + m_objects = objects; + + updateProperties(); +} + +QList ObjectPropertyGrid::selectedObjects() const +{ + return m_objects; +} + +QObject *ObjectPropertyGrid::selectedObject() const +{ + return m_objects.isEmpty() ? nullptr : m_objects.first(); +} + +void ObjectPropertyGrid::updateProperties() +{ + clearProperties(); + + if (m_objects.isEmpty()) + return; + + const QMetaObject *meta = m_objects.first()->metaObject(); + + for (int i = 0; i < meta->propertyCount(); ++i) + { + QMetaProperty prop = meta->property(i); + if (!prop.isReadable()) + continue; + + const QString name = prop.name(); + + // Check if property exists on all objects + bool common = true; + for (QObject *obj : qAsConst(m_objects)) + { + if (obj->metaObject()->indexOfProperty(name.toUtf8().constData()) < 0) + { + common = false; + break; + } + } + if (!common) + continue; + + // Determine merged value + QVariant firstValue = m_objects.first()->property(name.toUtf8().constData()); + bool allSame = true; + for (QObject *obj : qAsConst(m_objects)) + { + if (obj->property(name.toUtf8().constData()) != firstValue) + { + allSame = false; + + break; + } + } + + PM::Property property(name, prop.type()); + const QVariant displayValue = allSame ? firstValue : QVariant(); // blank if different + addProperty(property, displayValue); + + // Connect changes back to all objects + connect(this, &PM::PropertyGrid::propertyValueChanged, this, + [this, name](const PM::PropertyContext &context) + { + for (QObject *obj : qAsConst(m_objects)) + obj->setProperty(name.toUtf8().constData(), context.value()); + }); + } +} diff --git a/examples/object_property_grid/ObjectPropertyGrid.h b/examples/object_property_grid/ObjectPropertyGrid.h new file mode 100644 index 0000000..8329f5f --- /dev/null +++ b/examples/object_property_grid/ObjectPropertyGrid.h @@ -0,0 +1,25 @@ +#ifndef OBJECTPROPERTYGRID_H +#define OBJECTPROPERTYGRID_H + +#include + +class ObjectPropertyGrid : public PM::PropertyGrid +{ + Q_OBJECT +public: + explicit ObjectPropertyGrid(QWidget *parent = nullptr); + + QList selectedObjects() const; + void setSelectedObject(QObject *object); + + QObject *selectedObject() const; + void setSelectedObjects(const QList &objects); + +private: + void updateProperties(); + +private: + QList m_objects; +}; + +#endif // OBJECTPROPERTYGRID_H diff --git a/examples/object_property_grid/main.cpp b/examples/object_property_grid/main.cpp new file mode 100644 index 0000000..c9ea9f9 --- /dev/null +++ b/examples/object_property_grid/main.cpp @@ -0,0 +1,31 @@ +#include "Car.h" +#include "ObjectPropertyGrid.h" + +#include + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + Car *car1 = new Car(); + car1->setBrand("Tesla"); + car1->setYear(2025); + car1->setElectric(true); + + Car *car2 = new Car(); + car2->setBrand("BMW"); + car2->setYear(2025); + car2->setElectric(true); + + ObjectPropertyGrid *grid = new ObjectPropertyGrid(); + + // Single object + grid->setSelectedObject(car1); + + // Multiple objects + grid->setSelectedObjects({car1, car2}); + + grid->show(); + + return app.exec(); +} diff --git a/examples/property_grid_showcase/CMakeLists.txt b/examples/property_grid_showcase/CMakeLists.txt index f6b9f20..3d61281 100644 --- a/examples/property_grid_showcase/CMakeLists.txt +++ b/examples/property_grid_showcase/CMakeLists.txt @@ -11,5 +11,6 @@ add_executable(property_grid_showcase ) target_link_libraries(property_grid_showcase - PM::PropertyGrid + PRIVATE + PM::PropertyGrid ) From f267fb574ed460ed62035ae24445faf3057ebb32 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Tue, 25 Nov 2025 00:56:26 +0200 Subject: [PATCH 15/18] PM-183 refactored ObjectPropertyGrid::updateProperties() --- .../ObjectPropertyGrid.cpp | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/examples/object_property_grid/ObjectPropertyGrid.cpp b/examples/object_property_grid/ObjectPropertyGrid.cpp index 9e3f0fa..4af3a14 100644 --- a/examples/object_property_grid/ObjectPropertyGrid.cpp +++ b/examples/object_property_grid/ObjectPropertyGrid.cpp @@ -2,6 +2,58 @@ #include +// Return list of all common readable properties across selected objects +static QList getCommonProperties(const QList &objects) +{ + QList result; + if (objects.isEmpty()) + return result; + + const QMetaObject *meta = objects.first()->metaObject(); + for (int i = 0; i < meta->propertyCount(); ++i) + { + QMetaProperty prop = meta->property(i); + if (!prop.isReadable()) + continue; + + const char *name = prop.name(); + bool common = true; + for (QObject *obj : objects) + { + if (obj->metaObject()->indexOfProperty(name) >= 0) + continue; + + common = false; + break; + } + + if (common) + result.append(prop); + } + + return result; +} + +// Return merged value for a given property across selected objects +static QVariant getMergedPropertyValue(const QList &objects, const QMetaProperty &prop) +{ + if (objects.isEmpty()) + return QVariant(); + + const char *name = prop.name(); + QVariant firstValue = objects.first()->property(name); + + for (QObject *obj : objects) + { + if (obj->property(name) == firstValue) + continue; + + return QVariant(); // blank if different + } + + return firstValue; +} + ObjectPropertyGrid::ObjectPropertyGrid(QWidget *parent) : PM::PropertyGrid(parent) { } @@ -40,45 +92,15 @@ void ObjectPropertyGrid::updateProperties() if (m_objects.isEmpty()) return; - const QMetaObject *meta = m_objects.first()->metaObject(); + // Use helper functions + const QList commonProperties = getCommonProperties(m_objects); - for (int i = 0; i < meta->propertyCount(); ++i) + for (const QMetaProperty &property : commonProperties) { - QMetaProperty prop = meta->property(i); - if (!prop.isReadable()) - continue; - - const QString name = prop.name(); - - // Check if property exists on all objects - bool common = true; - for (QObject *obj : qAsConst(m_objects)) - { - if (obj->metaObject()->indexOfProperty(name.toUtf8().constData()) < 0) - { - common = false; - break; - } - } - if (!common) - continue; - - // Determine merged value - QVariant firstValue = m_objects.first()->property(name.toUtf8().constData()); - bool allSame = true; - for (QObject *obj : qAsConst(m_objects)) - { - if (obj->property(name.toUtf8().constData()) != firstValue) - { - allSame = false; - - break; - } - } + const QString name = property.name(); + const QVariant propertyValue = getMergedPropertyValue(m_objects, property); - PM::Property property(name, prop.type()); - const QVariant displayValue = allSame ? firstValue : QVariant(); // blank if different - addProperty(property, displayValue); + addProperty(PM::Property(name, property.type()), propertyValue); // Connect changes back to all objects connect(this, &PM::PropertyGrid::propertyValueChanged, this, From 07434e38654a68209ed5836b44d73de18057ec68 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Tue, 25 Nov 2025 01:23:38 +0200 Subject: [PATCH 16/18] PM-183 Added MainWindow --- examples/object_property_grid/CMakeLists.txt | 1 + examples/object_property_grid/MainWindow.cpp | 12 + examples/object_property_grid/MainWindow.h | 23 ++ examples/object_property_grid/MainWindow.ui | 220 +++++++++++++++++++ examples/object_property_grid/main.cpp | 36 +-- 5 files changed, 276 insertions(+), 16 deletions(-) create mode 100644 examples/object_property_grid/MainWindow.cpp create mode 100644 examples/object_property_grid/MainWindow.h create mode 100644 examples/object_property_grid/MainWindow.ui diff --git a/examples/object_property_grid/CMakeLists.txt b/examples/object_property_grid/CMakeLists.txt index efedd4c..4985653 100644 --- a/examples/object_property_grid/CMakeLists.txt +++ b/examples/object_property_grid/CMakeLists.txt @@ -10,6 +10,7 @@ add_executable(object_property_grid main.cpp ObjectPropertyGrid.h ObjectPropertyGrid.cpp Car.h Car.cpp + MainWindow.h MainWindow.cpp MainWindow.ui ) target_link_libraries(object_property_grid diff --git a/examples/object_property_grid/MainWindow.cpp b/examples/object_property_grid/MainWindow.cpp new file mode 100644 index 0000000..048ca6d --- /dev/null +++ b/examples/object_property_grid/MainWindow.cpp @@ -0,0 +1,12 @@ +#include "MainWindow.h" +#include "ui_MainWindow.h" + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) +{ + ui->setupUi(this); +} + +MainWindow::~MainWindow() +{ + delete ui; +} diff --git a/examples/object_property_grid/MainWindow.h b/examples/object_property_grid/MainWindow.h new file mode 100644 index 0000000..0c6dc80 --- /dev/null +++ b/examples/object_property_grid/MainWindow.h @@ -0,0 +1,23 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include + +namespace Ui +{ +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + Ui::MainWindow *ui; +}; + +#endif // MAINWINDOW_H diff --git a/examples/object_property_grid/MainWindow.ui b/examples/object_property_grid/MainWindow.ui new file mode 100644 index 0000000..a9b89ed --- /dev/null +++ b/examples/object_property_grid/MainWindow.ui @@ -0,0 +1,220 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 30 + 20 + 191 + 22 + + + + lineEdit + + + + + + 30 + 50 + 191 + 22 + + + + + Item 1 + + + + + Item 2 + + + + + Item 3 + + + + + + + 30 + 80 + 191 + 22 + + + + + + + 30 + 110 + 75 + 24 + + + + PushButton + + + + + + 30 + 140 + 22 + 22 + + + + ... + + + + + + 30 + 170 + 76 + 20 + + + + CheckBox + + + + + + 30 + 200 + 185 + 41 + + + + CommandLinkButton + + + + + + 20 + 250 + 201 + 111 + + + + Options + + + + + 10 + 30 + 89 + 20 + + + + Option1 + + + + + + 10 + 50 + 89 + 20 + + + + Option2 + + + + + + 10 + 70 + 89 + 20 + + + + Option3 + + + + + + + + 0 + 0 + 800 + 22 + + + + + + + + 300 + 95 + + + + Objects Tree + + + 2 + + + + + 1 + + + + + + + + 300 + 40 + + + + Properties + + + 2 + + + + + + + diff --git a/examples/object_property_grid/main.cpp b/examples/object_property_grid/main.cpp index c9ea9f9..2660893 100644 --- a/examples/object_property_grid/main.cpp +++ b/examples/object_property_grid/main.cpp @@ -1,5 +1,6 @@ -#include "Car.h" -#include "ObjectPropertyGrid.h" +// #include "Car.h" +// #include "ObjectPropertyGrid.h" +#include "MainWindow.h" #include @@ -7,25 +8,28 @@ int main(int argc, char **argv) { QApplication app(argc, argv); - Car *car1 = new Car(); - car1->setBrand("Tesla"); - car1->setYear(2025); - car1->setElectric(true); + MainWindow mainWindow; + mainWindow.show(); - Car *car2 = new Car(); - car2->setBrand("BMW"); - car2->setYear(2025); - car2->setElectric(true); + // Car *car1 = new Car(); + // car1->setBrand("Tesla"); + // car1->setYear(2025); + // car1->setElectric(true); - ObjectPropertyGrid *grid = new ObjectPropertyGrid(); + // Car *car2 = new Car(); + // car2->setBrand("BMW"); + // car2->setYear(2025); + // car2->setElectric(true); - // Single object - grid->setSelectedObject(car1); + // ObjectPropertyGrid *grid = new ObjectPropertyGrid(); - // Multiple objects - grid->setSelectedObjects({car1, car2}); + // // Single object + // grid->setSelectedObject(car1); - grid->show(); + // // Multiple objects + // grid->setSelectedObjects({car1, car2}); + + // grid->show(); return app.exec(); } From fb4d285b1083d68d500decec9469da11729b95c4 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Tue, 25 Nov 2025 01:50:57 +0200 Subject: [PATCH 17/18] PM-183 Added MainWindow logic --- examples/object_property_grid/MainWindow.cpp | 65 ++++++++++++++++++- examples/object_property_grid/MainWindow.h | 7 ++ examples/object_property_grid/MainWindow.ui | 1 - .../ObjectPropertyGrid.cpp | 17 +++-- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/examples/object_property_grid/MainWindow.cpp b/examples/object_property_grid/MainWindow.cpp index 048ca6d..2e2c238 100644 --- a/examples/object_property_grid/MainWindow.cpp +++ b/examples/object_property_grid/MainWindow.cpp @@ -1,12 +1,75 @@ #include "MainWindow.h" #include "ui_MainWindow.h" -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) +#include +#include + +static void addWidgetToTree(QTreeWidgetItem *parentItem, QWidget *widget) +{ + if (!widget) + return; + + auto *item = new QTreeWidgetItem(parentItem, {widget->objectName()}); + item->setData(0, Qt::UserRole, QVariant::fromValue(static_cast(widget))); + + const QList children = widget->findChildren(QString(), Qt::FindDirectChildrenOnly); + for (QWidget *child : children) + addWidgetToTree(item, child); +} + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), propertyGrid(this) { ui->setupUi(this); + + // Place propertyGrid inside the Properties dock + propertyGrid.layout()->setContentsMargins(0, 0, 0, 0); + ui->dockWidget_2->setWidget(&propertyGrid); + + // Populate tree with central widget hierarchy + ui->treeWidget->setHeaderLabel("Objects"); + ui->treeWidget->setSelectionMode(QAbstractItemView::ExtendedSelection); + + // Instead of adding the central widget itself, add its direct children + const QList children = ui->centralwidget->findChildren(QString(), Qt::FindDirectChildrenOnly); + for (QWidget *child : children) + addWidgetToTree(ui->treeWidget->invisibleRootItem(), child); + + // Expand everything by default + ui->treeWidget->expandAll(); + + // Default selection: first item + if (ui->treeWidget->topLevelItemCount() > 0) + { + ui->treeWidget->setCurrentItem(ui->treeWidget->topLevelItem(0)); + QObject *obj = ui->treeWidget->topLevelItem(0)->data(0, Qt::UserRole).value(); + + if (obj) + propertyGrid.setSelectedObject(obj); + } + + // Sync selection with property grid + connect(ui->treeWidget, &QTreeWidget::itemSelectionChanged, this, &MainWindow::objectsTreeSelectionChanged); } MainWindow::~MainWindow() { delete ui; } + +void MainWindow::objectsTreeSelectionChanged() +{ + QList selectedObjects; + for (QTreeWidgetItem *item : ui->treeWidget->selectedItems()) + { + QObject *obj = item->data(0, Qt::UserRole).value(); + if (obj) + selectedObjects.append(obj); + } + + if (selectedObjects.isEmpty()) + propertyGrid.setSelectedObjects({}); + else if (selectedObjects.size() == 1) + propertyGrid.setSelectedObject(selectedObjects.first()); + else + propertyGrid.setSelectedObjects(selectedObjects); +} diff --git a/examples/object_property_grid/MainWindow.h b/examples/object_property_grid/MainWindow.h index 0c6dc80..7dc62d7 100644 --- a/examples/object_property_grid/MainWindow.h +++ b/examples/object_property_grid/MainWindow.h @@ -1,6 +1,8 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H +#include "ObjectPropertyGrid.h" + #include namespace Ui @@ -16,8 +18,13 @@ class MainWindow : public QMainWindow explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); +private slots: + void objectsTreeSelectionChanged(); + private: Ui::MainWindow *ui; + + ObjectPropertyGrid propertyGrid; }; #endif // MAINWINDOW_H diff --git a/examples/object_property_grid/MainWindow.ui b/examples/object_property_grid/MainWindow.ui index a9b89ed..9185d55 100644 --- a/examples/object_property_grid/MainWindow.ui +++ b/examples/object_property_grid/MainWindow.ui @@ -212,7 +212,6 @@ 2 - diff --git a/examples/object_property_grid/ObjectPropertyGrid.cpp b/examples/object_property_grid/ObjectPropertyGrid.cpp index 4af3a14..ba11d0e 100644 --- a/examples/object_property_grid/ObjectPropertyGrid.cpp +++ b/examples/object_property_grid/ObjectPropertyGrid.cpp @@ -54,6 +54,16 @@ static QVariant getMergedPropertyValue(const QList &objects, const QM return firstValue; } +static PM::Property createGridProperty(const QMetaProperty &metaObjectProperty) +{ + PM::Property result(metaObjectProperty.name(), metaObjectProperty.type()); + + if (!metaObjectProperty.isWritable()) + result.addAttribute(PM::ReadOnlyAttribute()); + + return result; +} + ObjectPropertyGrid::ObjectPropertyGrid(QWidget *parent) : PM::PropertyGrid(parent) { } @@ -97,17 +107,16 @@ void ObjectPropertyGrid::updateProperties() for (const QMetaProperty &property : commonProperties) { - const QString name = property.name(); const QVariant propertyValue = getMergedPropertyValue(m_objects, property); - addProperty(PM::Property(name, property.type()), propertyValue); + addProperty(createGridProperty(property), propertyValue); // Connect changes back to all objects connect(this, &PM::PropertyGrid::propertyValueChanged, this, - [this, name](const PM::PropertyContext &context) + [this](const PM::PropertyContext &context) { for (QObject *obj : qAsConst(m_objects)) - obj->setProperty(name.toUtf8().constData(), context.value()); + obj->setProperty(context.property().name().toUtf8().constData(), context.value()); }); } } From 4431a019d4d9243978f3971929fd521520ab2f22 Mon Sep 17 00:00:00 2001 From: Mostafa Nanticock <47303284+MostafaNanticock@users.noreply.github.com> Date: Tue, 25 Nov 2025 03:00:35 +0200 Subject: [PATCH 18/18] PM-183 cleaned object_property_grid example --- README.md | 78 +++++++++--------- examples/object_property_grid/CMakeLists.txt | 2 +- examples/object_property_grid/Car.cpp | 47 ----------- examples/object_property_grid/Car.h | 36 -------- examples/object_property_grid/MainWindow.ui | 11 ++- .../ObjectPropertyGrid.cpp | 42 +++++----- .../object_property_grid/ObjectPropertyGrid.h | 9 +- examples/object_property_grid/README.md | 22 +++++ examples/object_property_grid/main.cpp | 22 ----- .../object_property_grid_example.png | Bin 0 -> 38138 bytes 10 files changed, 97 insertions(+), 172 deletions(-) delete mode 100644 examples/object_property_grid/Car.cpp delete mode 100644 examples/object_property_grid/Car.h create mode 100644 examples/object_property_grid/README.md create mode 100644 examples/object_property_grid/object_property_grid_example.png diff --git a/README.md b/README.md index 8003260..acebaf8 100644 --- a/README.md +++ b/README.md @@ -20,45 +20,6 @@ This library provides a flexible and customizable property editor that displays > > Feel free to use it, report issues, and contribute! The core functionality works well, and the API won't change in ways that break your code. -## Features - -- **Comprehensive Type Support**: Planned Built-in support for all major Qt types including: - - [x] **Basic types**: `int`, `bool`, `QString`, `double` - - [ ] **Geometric types**: `QPoint`, `QPointF`, `QSize`, `QSizeF`, `QRect`, `QRectF`, `QLine`, `QLineF` - - [x] **Graphics types**: `QColor`, `QFont`, `QIcon`, `QPixmap`, `QBitmap`, `QImage` - - [ ] **Advanced types**: `QVector2D`, `QVector3D`, `QVector4D`, `QMatrix4x4`, `QPolygon`, `QPolygonF` - - [x] **Date/Time types**: `QDate`, `QTime`, `QDateTime` - - [ ] **UI types** (partial): `QKeySequence`, `QCursor`, `QChar` - - [ ] **Object types**: `QObject*` and custom enums - -- **Property Attributes**: Rich attribute system for enhanced property configuration: - - `DescriptionAttribute`: Add helpful descriptions to properties - - `DefaultValueAttribute`: Define default values for properties - - `CategoryAttribute`: Organize properties into collapsible categories - - `ReadOnlyAttribute`: Mark properties as read-only - -- **Flexible API**: Multiple ways to add properties: - - Simple property addition with automatic type detection - - Detailed property configuration with attributes - - Batch property addition with categories - -- **Interactive Features**: - - Collapsible categories for better organization - - Real-time property value change notifications - - Support for custom property editors - - Responsive tree-based layout - -- **Cross-Platform**: Compatible with Qt 5.9+ on Windows, macOS, and Linux - -- **Wide Qt Compatibility**: Designed to work with all Qt versions from Qt 5.9 onward, ensuring maximum compatibility across projects - -## Requirements - -- **CMake**: 3.10 or higher -- **Qt**: 5.9+ (Widgets module required) -- **C++ Standard**: C++17 (can work on C++14 if you have a replacement implementation for `std::variant`) -- **Compiler**: MSVC 2019, GCC, or Clang with C++17 support - ## Quick Start ```cpp @@ -99,6 +60,45 @@ int main(int argc, char *argv[]) } ``` +## Features + +- **Comprehensive Type Support**: Planned Built-in support for all major Qt types including: + - [x] **Basic types**: `int`, `bool`, `QString`, `double` + - [ ] **Geometric types**: `QPoint`, `QPointF`, `QSize`, `QSizeF`, `QRect`, `QRectF`, `QLine`, `QLineF` + - [x] **Graphics types**: `QColor`, `QFont`, `QIcon`, `QPixmap`, `QBitmap`, `QImage` + - [ ] **Advanced types**: `QVector2D`, `QVector3D`, `QVector4D`, `QMatrix4x4`, `QPolygon`, `QPolygonF` + - [x] **Date/Time types**: `QDate`, `QTime`, `QDateTime` + - [ ] **UI types** (partial): `QKeySequence`, `QCursor`, `QChar` + - [ ] **Object types**: `QObject*` and custom enums + +- **Property Attributes**: Rich attribute system for enhanced property configuration: + - `DescriptionAttribute`: Add helpful descriptions to properties + - `DefaultValueAttribute`: Define default values for properties + - `CategoryAttribute`: Organize properties into collapsible categories + - `ReadOnlyAttribute`: Mark properties as read-only + +- **Flexible API**: Multiple ways to add properties: + - Simple property addition with automatic type detection + - Detailed property configuration with attributes + - Batch property addition with categories + +- **Interactive Features**: + - Collapsible categories for better organization + - Real-time property value change notifications + - Support for custom property editors + - Responsive tree-based layout + +- **Cross-Platform**: Compatible with Qt 5.9+ on Windows, macOS, and Linux + +- **Wide Qt Compatibility**: Designed to work with all Qt versions from Qt 5.9 onward, ensuring maximum compatibility across projects + +## Requirements + +- **CMake**: 3.10 or higher +- **Qt**: 5.9+ (Widgets module required) +- **C++ Standard**: C++17 (can work on C++14 if you have a replacement implementation for `std::variant`) +- **Compiler**: MSVC 2019, GCC, or Clang with C++17 support + ## API Overview ### Core Classes diff --git a/examples/object_property_grid/CMakeLists.txt b/examples/object_property_grid/CMakeLists.txt index 4985653..fa9dee1 100644 --- a/examples/object_property_grid/CMakeLists.txt +++ b/examples/object_property_grid/CMakeLists.txt @@ -9,7 +9,7 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core) add_executable(object_property_grid main.cpp ObjectPropertyGrid.h ObjectPropertyGrid.cpp - Car.h Car.cpp + MainWindow.h MainWindow.cpp MainWindow.ui ) diff --git a/examples/object_property_grid/Car.cpp b/examples/object_property_grid/Car.cpp deleted file mode 100644 index f67d53e..0000000 --- a/examples/object_property_grid/Car.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "Car.h" - -Car::Car(QObject *parent) : QObject(parent), m_electric(false), m_year(2025) -{ -} - -QString Car::brand() const -{ - return m_brand; -} - -void Car::setBrand(const QString &b) -{ - if (m_brand == b) - return; - - m_brand = b; - emit brandChanged(); -} - -int Car::year() const -{ - return m_year; -} - -void Car::setYear(int y) -{ - if (m_year == y) - return; - - m_year = y; - emit yearChanged(); -} - -bool Car::electric() const -{ - return m_electric; -} - -void Car::setElectric(bool e) -{ - if (m_electric != e) - return; - - m_electric = e; - emit electricChanged(); -} diff --git a/examples/object_property_grid/Car.h b/examples/object_property_grid/Car.h deleted file mode 100644 index 989a490..0000000 --- a/examples/object_property_grid/Car.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef CAR_H -#define CAR_H - -#include - -class Car : public QObject -{ - Q_OBJECT - Q_PROPERTY(QString brand READ brand WRITE setBrand NOTIFY brandChanged) - Q_PROPERTY(int year READ year WRITE setYear NOTIFY yearChanged) - Q_PROPERTY(bool electric READ electric WRITE setElectric NOTIFY electricChanged) - -public: - Car(QObject *parent = nullptr); - - QString brand() const; - void setBrand(const QString &b); - - int year() const; - void setYear(int y); - - bool electric() const; - void setElectric(bool e); - -signals: - void brandChanged(); - void yearChanged(); - void electricChanged(); - -private: - QString m_brand; - bool m_electric; - int m_year; -}; - -#endif // CAR_H diff --git a/examples/object_property_grid/MainWindow.ui b/examples/object_property_grid/MainWindow.ui index 9185d55..b549c6c 100644 --- a/examples/object_property_grid/MainWindow.ui +++ b/examples/object_property_grid/MainWindow.ui @@ -7,11 +7,11 @@ 0 0 800 - 600 + 800 - MainWindow + ObjectsPropertyGrid example @@ -138,6 +138,9 @@ Option1 + + true + @@ -181,7 +184,7 @@ - 300 + 500 95 @@ -202,7 +205,7 @@ - 300 + 500 40 diff --git a/examples/object_property_grid/ObjectPropertyGrid.cpp b/examples/object_property_grid/ObjectPropertyGrid.cpp index ba11d0e..976e330 100644 --- a/examples/object_property_grid/ObjectPropertyGrid.cpp +++ b/examples/object_property_grid/ObjectPropertyGrid.cpp @@ -3,7 +3,8 @@ #include // Return list of all common readable properties across selected objects -static QList getCommonProperties(const QList &objects) +// only properties that are common between all objects gets returned +static QList getCommonProperties(const QObjectList &objects) { QList result; if (objects.isEmpty()) @@ -35,13 +36,14 @@ static QList getCommonProperties(const QList &objects) } // Return merged value for a given property across selected objects -static QVariant getMergedPropertyValue(const QList &objects, const QMetaProperty &prop) +// If the property value is the same then we return it, if it's different we return an invalid variant +static QVariant getMergedPropertyValue(const QObjectList &objects, const QMetaProperty &prop) { if (objects.isEmpty()) return QVariant(); const char *name = prop.name(); - QVariant firstValue = objects.first()->property(name); + const QVariant firstValue = objects.first()->property(name); for (QObject *obj : objects) { @@ -78,14 +80,14 @@ void ObjectPropertyGrid::setSelectedObject(QObject *object) updateProperties(); } -void ObjectPropertyGrid::setSelectedObjects(const QList &objects) +void ObjectPropertyGrid::setSelectedObjects(const QObjectList &objects) { m_objects = objects; updateProperties(); } -QList ObjectPropertyGrid::selectedObjects() const +QObjectList ObjectPropertyGrid::selectedObjects() const { return m_objects; } @@ -99,24 +101,26 @@ void ObjectPropertyGrid::updateProperties() { clearProperties(); + for (const auto &connection : qAsConst(m_objectsConnections)) + disconnect(connection); + m_objectsConnections.clear(); + if (m_objects.isEmpty()) return; - // Use helper functions const QList commonProperties = getCommonProperties(m_objects); - - for (const QMetaProperty &property : commonProperties) + for (const QMetaProperty &metaProperty : commonProperties) { - const QVariant propertyValue = getMergedPropertyValue(m_objects, property); - - addProperty(createGridProperty(property), propertyValue); - - // Connect changes back to all objects - connect(this, &PM::PropertyGrid::propertyValueChanged, this, - [this](const PM::PropertyContext &context) - { - for (QObject *obj : qAsConst(m_objects)) - obj->setProperty(context.property().name().toUtf8().constData(), context.value()); - }); + const PM::Property gridProperty = createGridProperty(metaProperty); + const QVariant propertyValue = getMergedPropertyValue(m_objects, metaProperty); + addProperty(gridProperty, propertyValue); + + // make sure to propagate properties values that are changed in the UI to the underlying objects + m_objectsConnections << connect(this, &PM::PropertyGrid::propertyValueChanged, this, + [this](const PM::PropertyContext &context) + { + for (QObject *obj : qAsConst(m_objects)) + obj->setProperty(context.property().name().toUtf8().constData(), context.value()); + }); } } diff --git a/examples/object_property_grid/ObjectPropertyGrid.h b/examples/object_property_grid/ObjectPropertyGrid.h index 8329f5f..cc23cf4 100644 --- a/examples/object_property_grid/ObjectPropertyGrid.h +++ b/examples/object_property_grid/ObjectPropertyGrid.h @@ -9,17 +9,18 @@ class ObjectPropertyGrid : public PM::PropertyGrid public: explicit ObjectPropertyGrid(QWidget *parent = nullptr); - QList selectedObjects() const; + QObject *selectedObject() const; void setSelectedObject(QObject *object); - QObject *selectedObject() const; - void setSelectedObjects(const QList &objects); + QObjectList selectedObjects() const; + void setSelectedObjects(const QObjectList &objects); private: void updateProperties(); private: - QList m_objects; + QObjectList m_objects; + QVector m_objectsConnections; }; #endif // OBJECTPROPERTYGRID_H diff --git a/examples/object_property_grid/README.md b/examples/object_property_grid/README.md new file mode 100644 index 0000000..830f93b --- /dev/null +++ b/examples/object_property_grid/README.md @@ -0,0 +1,22 @@ +# Object Property Grid Example + +![Preview](object_property_grid_example.png) + +## Overview +This example demonstrates how to integrate a custom **ObjectPropertyGrid** into a Qt application. The property grid allows inspection and editing of common properties across multiple selected `QObject` instances. It is designed to behave similarly to the property grid found in development environments, showing shared properties and synchronizing changes back to the selected objects. + +## Features +- Uses `QMetaProperty` introspection to dynamically build the grid. +- Displays only properties that are common across all selected objects. +- Supports both single and multiple object selection. +- Updates property values directly on the underlying `QObject` instances. + +## How It Works +1. The tree view lists child widgets of the central widget. +2. Selecting one or more items updates the property grid with common properties. +3. Editing a property in the grid applies the change to all selected objects. +4. Connections are cleared and rebuilt whenever the selection changes to avoid stale signal bindings. + +## Structure +- **MainWindow**: Demonstrates usage by populating a tree of widgets and connecting selection changes to the property grid. +- **ObjectPropertyGrid**: Inherits from `PM::PropertyGrid` and manages the selected objects and property display. \ No newline at end of file diff --git a/examples/object_property_grid/main.cpp b/examples/object_property_grid/main.cpp index 2660893..acdc223 100644 --- a/examples/object_property_grid/main.cpp +++ b/examples/object_property_grid/main.cpp @@ -1,5 +1,3 @@ -// #include "Car.h" -// #include "ObjectPropertyGrid.h" #include "MainWindow.h" #include @@ -11,25 +9,5 @@ int main(int argc, char **argv) MainWindow mainWindow; mainWindow.show(); - // Car *car1 = new Car(); - // car1->setBrand("Tesla"); - // car1->setYear(2025); - // car1->setElectric(true); - - // Car *car2 = new Car(); - // car2->setBrand("BMW"); - // car2->setYear(2025); - // car2->setElectric(true); - - // ObjectPropertyGrid *grid = new ObjectPropertyGrid(); - - // // Single object - // grid->setSelectedObject(car1); - - // // Multiple objects - // grid->setSelectedObjects({car1, car2}); - - // grid->show(); - return app.exec(); } diff --git a/examples/object_property_grid/object_property_grid_example.png b/examples/object_property_grid/object_property_grid_example.png new file mode 100644 index 0000000000000000000000000000000000000000..6abe378cea997f1a7b94bff438d83f78d13137f5 GIT binary patch literal 38138 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 Q>_1H9f*QO)-Z