From 8da1ec6a093ecf3ae2b8492a122e5822ec1aa532 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Wed, 26 Feb 2025 11:07:20 +0100 Subject: [PATCH] QGIM: support gadgets and objects as rows and multi-role values We can interpret the properties of a gadget or object in two ways: either as different columns in a fixed-width table, or as different role-values in a single item, matching the named properties against the role names. If used in a table structure, then the properties can only be interpreted as different role values, as we already have a row type with multiple columns. In a list it can be either. To disambiguate in this case, check whether there is a "display" property. If so, then we assume that the gadget is supposed to be a multi-role item. Change-Id: I99400304986f643506e590c5287f4d800654ec77 Reviewed-by: Artem Dyomin --- .../doc/snippets/qgenericitemmodel/main.cpp | 77 +++++++ src/corelib/itemmodels/qgenericitemmodel.cpp | 31 +++ src/corelib/itemmodels/qgenericitemmodel.h | 198 ++++++++++++++++-- .../itemmodels/qgenericitemmodel_impl.h | 19 ++ .../tst_qgenericitemmodel.cpp | 105 +++++++++- 5 files changed, 407 insertions(+), 23 deletions(-) diff --git a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp index 9251d3c1f0e..7b688508d4c 100644 --- a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp +++ b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp @@ -107,6 +107,41 @@ namespace std { //! [tuple_protocol] #endif // __cpp_concepts && forward_like +namespace gadget { +//! [gadget] +class Book +{ + Q_GADGET + Q_PROPERTY(QString title READ title) + Q_PROPERTY(QString author READ author) + Q_PROPERTY(QString summary MEMBER m_summary) + Q_PROPERTY(int rating READ rating WRITE setRating) +public: + Book(const QString &title, const QString &author); + + // C++ rule of 0: destructor, as well as copy/move operations + // provided by the compiler. + + // read-only properties + QString title() const { return m_title; } + QString author() const { return m_author; } + + // read/writable property with input validation + int rating() const { return m_rating; } + void setRating(int rating) + { + m_rating = qBound(0, rating, 5); + } + +private: + QString m_title; + QString m_author; + QString m_summary; + int m_rating = 0; +}; +//! [gadget] +} // namespace gadget + void color_map() { //! [color_map] @@ -126,3 +161,45 @@ QListView list; list.setModel(&colorModel); //! [color_map] } + +namespace multirole_gadget { +//! [color_gadget_0] +class ColorEntry +{ + Q_GADGET + Q_PROPERTY(QString display MEMBER m_colorName) + Q_PROPERTY(QColor decoration READ decoration) + Q_PROPERTY(QString toolTip READ toolTip) +public: + ColorEntry(const QString &color = {}) + : m_colorName(color) + {} + + QColor decoration() const + { + return QColor::fromString(m_colorName); + } + QString toolTip() const + { + return QColor::fromString(m_colorName).name(); + } + +private: + QString m_colorName; +}; +//! [color_gadget_0] + +void color_list() { +//! [color_gadget_1] +const QStringList colorNames = QColor::colorNames(); +QList> colors; +colors.reserve(colorNames.size()); +for (const QString &name : colorNames) + colors << ColorEntry{name}; + +QGenericItemModel colorModel(colors); +QListView list; +list.setModel(&colorModel); +//! [color_gadget_1] +} +} // namespace multirole_gadget diff --git a/src/corelib/itemmodels/qgenericitemmodel.cpp b/src/corelib/itemmodels/qgenericitemmodel.cpp index 73229e38653..994d0e66fb3 100644 --- a/src/corelib/itemmodels/qgenericitemmodel.cpp +++ b/src/corelib/itemmodels/qgenericitemmodel.cpp @@ -91,6 +91,24 @@ QT_BEGIN_NAMESPACE \snippet qgenericitemmodel/main.cpp pair_int_QString + An easier and more flexible alternative to implementing the tuple protocol + for a C++ type is to use Qt's \l{Meta-Object System}{meta-object system} to + declare a type with \l{Qt's Property System}{properties}. This can be a + value type that is declared as a \l{Q_GADGET}{gadget}, or a QObject subclass. + + \snippet qgenericitemmodel/main.cpp gadget + + Using QObject subclasses allows properties to be \l{Qt Bindable Properties} + {bindable}, or to have change notification signals. However, using QObject + instances for items has significant memory overhead. + + Using Qt gadgets or objects is more convenient and can be more flexible + than implementing the tuple protocol. Those types are also directly + accessible from within QML. However, the access through the property system + comes with some runtime overhead. For performance critical models, consider + implementing the tuple protocol for compile-time generation of the access + code. + \section2 Multi-role items The type of the items that the implementations of data(), setData(), @@ -118,6 +136,19 @@ QT_BEGIN_NAMESPACE \c{int}. When using \c{int}, itemData() returns the container as is, and doesn't have to create a copy of the data. + Gadgets and QObject types are also represented at multi-role items if they + are the item type in a table. The names of the properties have to match the + names of the roles. + + \snippet qgenericitemmodel/main.cpp color_gadget_0 + + When used in a list, these types are ambiguous: they can be represented as + multi-column rows, with each property represented as a separate column. Or + they can be single items with each property being a role. To disambiguate, + use the QGenericItemModel::SingleColumn wrapper. + + \snippet qgenericitemmodel/main.cpp color_gadget_1 + \section2 The C++ tuple protocol As seen in the \c{numberNames} example above, the row type can be a tuple, diff --git a/src/corelib/itemmodels/qgenericitemmodel.h b/src/corelib/itemmodels/qgenericitemmodel.h index 8cd5f8a26ab..11856f3a838 100644 --- a/src/corelib/itemmodels/qgenericitemmodel.h +++ b/src/corelib/itemmodels/qgenericitemmodel.h @@ -13,6 +13,9 @@ class Q_CORE_EXPORT QGenericItemModel : public QAbstractItemModel { Q_OBJECT public: + template + using SingleColumn = std::tuple; + template = true> explicit QGenericItemModel(Range &&range, QObject *parent = nullptr); @@ -143,6 +146,11 @@ protected: using row_ptr = std::conditional_t; using const_row_ptr = const std::remove_pointer_t *; + template + static constexpr bool has_metaobject = + (QtPrivate::QMetaTypeForType>::flags() & QMetaType::IsGadget) + || (QtPrivate::QMetaTypeForType::flags() & QMetaType::PointerToQObject); + using ModelData = QGenericItemModelDetails::ModelData, Range, std::remove_reference_t> @@ -217,7 +225,14 @@ public: Qt::ItemFlags f = Structure::defaultFlags(); - if constexpr (static_column_count <= 0) { + if constexpr (has_metaobject) { + if (index.column() < row_traits::fixed_size()) { + const QMetaObject mo = std::remove_pointer_t::staticMetaObject; + const QMetaProperty prop = mo.property(index.column() + mo.propertyOffset()); + if (prop.isWritable()) + f |= Qt::ItemIsEditable; + } + } else if constexpr (static_column_count <= 0) { if constexpr (isMutable()) f |= Qt::ItemIsEditable; } else if constexpr (std::is_reference_v && !std::is_const_v) { @@ -244,7 +259,17 @@ public: return m_itemModel->QAbstractItemModel::headerData(section, orientation, role); } - if constexpr (static_column_count >= 1) { + if constexpr (has_metaobject) { + using meta_type = std::remove_pointer_t; + if (row_traits::fixed_size() == 1) { + const QMetaType metaType = QMetaType::fromType(); + result = QString::fromUtf8(metaType.name()); + } else if (section <= row_traits::fixed_size()) { + const QMetaProperty prop = meta_type::staticMetaObject.property( + section + meta_type::staticMetaObject.propertyOffset()); + result = QString::fromUtf8(prop.name()); + } + } else if constexpr (static_column_count >= 1) { const QMetaType metaType = meta_type_at(section); if (metaType.isValid()) result = QString::fromUtf8(metaType.name()); @@ -257,11 +282,18 @@ public: QVariant data(const QModelIndex &index, int role) const { QVariant result; - const auto readData = [this, &result, role](const auto &value) { + const auto readData = [this, column = index.column(), &result, role](const auto &value) { Q_UNUSED(this); using value_type = q20::remove_cvref_t; using multi_role = QGenericItemModelDetails::is_multi_role; - if constexpr (multi_role::value) { + if constexpr (has_metaobject) { + if (row_traits::fixed_size() <= 1) { + result = readRole(role, value); + } else if (column <= row_traits::fixed_size() + && (role == Qt::DisplayRole || role == Qt::EditRole)) { + result = readProperty(column, value); + } + } else if constexpr (multi_role::value) { const auto it = [this, &value, role]{ Q_UNUSED(this); if constexpr (multi_role::int_key) @@ -347,10 +379,20 @@ public: } }); - const auto writeData = [this, &data, role](auto &&target) -> bool { + const auto writeData = [this, column = index.column(), &data, role](auto &&target) -> bool { using value_type = q20::remove_cvref_t; using multi_role = QGenericItemModelDetails::is_multi_role; - if constexpr (multi_role::value) { + if constexpr (has_metaobject) { + if (QMetaType::fromType() == data.metaType()) { + target = data.value(); + return true; + } else if (row_traits::fixed_size() <= 1) { + return writeRole(role, target, data); + } else if (column <= row_traits::fixed_size() + && (role == Qt::DisplayRole || role == Qt::EditRole)) { + return writeProperty(column, target, data); + } + } else if constexpr (multi_role::value) { Qt::ItemDataRole roleToSet = Qt::ItemDataRole(role); // If there is an entry for EditRole, overwrite that; otherwise, // set the entry for DisplayRole. @@ -476,20 +518,32 @@ public: Q_EMIT dataChanged(index, index, {}); }); + auto clearData = [column = index.column()](auto &&target) { + if constexpr (has_metaobject) { + if (row_traits::fixed_size() <= 1) { + // multi-role object/gadget: reset all properties + return resetProperty(-1, target); + } else if (column <= row_traits::fixed_size()) { + return resetProperty(column, target); + } + } else { // normal structs, values, associative containers + target = {}; + return true; + } + return false; + }; + row_reference row = rowData(index); if constexpr (dynamicColumns()) { - *std::next(std::begin(row), index.column()) = {}; - success = true; + success = clearData(*std::next(std::begin(row), index.column())); } else if constexpr (static_column_count == 0) { - row = row_type{}; - success = true; + success = clearData(row); } else if (QGenericItemModelDetails::isValid(row)) { - for_element_at(row, index.column(), [&success](auto &&target){ + for_element_at(row, index.column(), [&clearData, &success](auto &&target){ using target_type = decltype(target); if constexpr (std::is_lvalue_reference_v && !std::is_const_v>) { - target = {}; - success = true; + success = clearData(target); } }); } @@ -649,6 +703,124 @@ protected: return false; } + template + QMetaProperty roleProperty(int role) const + { + const QMetaObject *mo = &ItemType::staticMetaObject; + const QByteArray roleName = roleNames().value(role); + if (const int index = mo->indexOfProperty(roleName.data()); index >= 0) + return mo->property(index); + return {}; + } + + template + QVariant readRole(int role, ItemType *gadget) const + { + using item_type = std::remove_pointer_t; + QVariant result; + QMetaProperty prop = roleProperty(role); + if (!prop.isValid() && role == Qt::EditRole) + prop = roleProperty(Qt::DisplayRole); + + if (prop.isValid()) + result = readProperty(prop, gadget); + return result; + } + + template + QVariant readRole(int role, const ItemType &gadget) const + { + return readRole(role, &gadget); + } + + template + static QVariant readProperty(const QMetaProperty &prop, ItemType *gadget) + { + if constexpr (std::is_base_of_v) + return prop.read(gadget); + else + return prop.readOnGadget(gadget); + } + template + static QVariant readProperty(int property, ItemType *gadget) + { + using item_type = std::remove_pointer_t; + const QMetaObject &mo = item_type::staticMetaObject; + const QMetaProperty prop = mo.property(property + mo.propertyOffset()); + return readProperty(prop, gadget); + } + + template + static QVariant readProperty(int property, const ItemType &gadget) + { + return readProperty(property, &gadget); + } + + template + bool writeRole(int role, ItemType *gadget, const QVariant &data) + { + using item_type = std::remove_pointer_t; + auto prop = roleProperty(role); + if (!prop.isValid() && role == Qt::EditRole) + prop = roleProperty(Qt::DisplayRole); + + return writeProperty(prop, gadget, data); + } + + template + bool writeRole(int role, ItemType &&gadget, const QVariant &data) + { + return writeRole(role, &gadget, data); + } + + template + static bool writeProperty(const QMetaProperty &prop, ItemType *gadget, const QVariant &data) + { + if constexpr (std::is_base_of_v) + return prop.write(gadget, data); + else + return prop.writeOnGadget(gadget, data); + } + template + static bool writeProperty(int property, ItemType *gadget, const QVariant &data) + { + using item_type = std::remove_pointer_t; + const QMetaObject &mo = item_type::staticMetaObject; + return writeProperty(mo.property(property + mo.propertyOffset()), gadget, data); + } + + template + static bool writeProperty(int property, ItemType &&gadget, const QVariant &data) + { + return writeProperty(property, &gadget, data); + } + + template + static bool resetProperty(int property, ItemType *object) + { + using item_type = std::remove_pointer_t; + const QMetaObject &mo = item_type::staticMetaObject; + bool success = true; + if (property == -1) { + // reset all properties + if constexpr (std::is_base_of_v) { + for (int p = mo.propertyOffset(); p < mo.propertyCount(); ++p) + success = writeProperty(mo.property(p), object, {}) && success; + } else { // reset a gadget by assigning a default-constructed + *object = {}; + } + } else { + success = writeProperty(mo.property(property + mo.propertyOffset()), object, {}); + } + return success; + } + + template + static bool resetProperty(int property, ItemType &&object) + { + return resetProperty(property, &object); + } + // helpers const_row_reference rowData(const QModelIndex &index) const { diff --git a/src/corelib/itemmodels/qgenericitemmodel_impl.h b/src/corelib/itemmodels/qgenericitemmodel_impl.h index 577ffaec9dc..b5974e212e5 100644 --- a/src/corelib/itemmodels/qgenericitemmodel_impl.h +++ b/src/corelib/itemmodels/qgenericitemmodel_impl.h @@ -170,6 +170,25 @@ namespace QGenericItemModelDetails static constexpr int fixed_size() { return 0; } }; + // Specialization for gadgets + // clang doesn't accept multiple specializations using std::void_t directly + template struct make_void { using type = void; }; + + template + struct row_traits::type> + { + static constexpr int static_size = 0; + static int fixed_size(){ + // Interpret a gadget in a list as a multi-column row item. To + // disambiguate, stick it into a SingleColumn wrapper. + static const int columnCount = []{ + const QMetaObject &mo = T::staticMetaObject; + return mo.propertyCount() - mo.propertyOffset(); + }(); + return columnCount; + } + }; + template [[maybe_unused]] static constexpr int static_size_v = row_traits>>::static_size; diff --git a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp index 3f60a84119a..fbf980d6989 100644 --- a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp +++ b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp @@ -12,15 +12,62 @@ #include #endif +#include #include #if defined(__cpp_lib_ranges) #include #endif +class Item +{ + Q_GADGET + Q_PROPERTY(QString display READ display WRITE setDisplay) + Q_PROPERTY(QColor decoration READ decoration WRITE setDecoration) + Q_PROPERTY(QString toolTip READ toolTip WRITE setToolTip) +public: + Item() = default; + + Item(const QString &display, QColor decoration, const QString &toolTip) + : m_display(display), m_decoration(decoration), m_toolTip(toolTip) + { + } + + QString display() const { return m_display; } + void setDisplay(const QString &display) { m_display = display; } + QColor decoration() const { return m_decoration; } + void setDecoration(QColor decoration) { m_decoration = decoration; } + QString toolTip() const { return m_toolTip.isEmpty() ? display() : m_toolTip; } + void setToolTip(const QString &toolTip) { m_toolTip = toolTip; } + +private: + QString m_display; + QColor m_decoration; + QString m_toolTip; +}; + +class Object : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString string READ string WRITE setString) + Q_PROPERTY(int number READ number WRITE setNumber) +public: + using QObject::QObject; + + QString string() const { return m_string; } + void setString(const QString &string) { m_string = string; } + int number() const { return m_number; } + void setNumber(int number) { m_number = number; } + +private: + // note: default values need to be convertible to each other + QString m_string = "1234"; + int m_number = 42; +}; + struct Row { - QString m_item; + Item m_item; int m_number; QString m_description; @@ -41,7 +88,7 @@ struct Row namespace std { template <> struct tuple_size : std::integral_constant {}; - template <> struct tuple_element<0, Row> { using type = QString; }; + template <> struct tuple_element<0, Row> { using type = Item; }; template <> struct tuple_element<1, Row> { using type = int; }; template <> struct tuple_element<2, Row> { using type = QString; }; } @@ -110,9 +157,9 @@ private: std::array fixedArrayOfNumbers = {1, 2, 3, 4, 5}; int cArrayOfNumbers[5] = {1, 2, 3, 4, 5}; Row cArrayFixedColumns[3] = { - {"red", 0xff0000, "The color red"}, - {"green", 0x00ff00, "The color green"}, - {"blue", 0x0000ff, "The color blue"} + {{"red", Qt::red, "0xff0000"}, 0xff0000, "The color red"}, + {{"green", Qt::green, "0x00ff00"}, 0x00ff00, "The color green"}, + {{"blue", Qt::blue, "0x0000ff"}, 0x0000ff, "The color blue"} }; // dynamic number of rows, fixed number of columns @@ -130,10 +177,27 @@ private: {31, 32, 33, 34, 35, 36, 37, 38, 39, 40}, {41, 42, 43, 44, 45, 46, 47, 48, 49, 50}, }; + std::vector vectorOfGadgets = { + {"red", Qt::red, "0xff0000"}, + {"green", Qt::green, "0x00ff00"}, + {"blue", Qt::blue, "0x0000ff"}, + }; + std::vector> listOfGadgets = { + {{"red", Qt::red, "0xff0000"}}, + {{"green", Qt::green, "0x00ff00"}}, + {{"blue", Qt::blue, "0x0000ff"}}, + }; std::vector vectorOfStructs = { - {"red", 1, "one"}, - {"green", 2, "two"}, - {"blue", 3, "three"}, + {{"red", Qt::red, "0xff0000"}, 1, "one"}, + {{"green", Qt::green, "0x00ff00"}, 2, "two"}, + {{"blue", Qt::blue, "0x0000ff"}, 3, "three"}, + }; + + Object row1; + Object row2; + Object row3; + std::list listOfObjects = { + &row1, &row2, &row3 }; // bad (but legal) get() overload that never returns a mutable reference @@ -152,8 +216,16 @@ private: {21.0, 22.0, 23.0, 24.0, 25.0}, }; + // item is pointer + Item itemAsPointer = {"red", Qt::red, "0xff0000"}; + std::vector> tableOfPointers = { + {&itemAsPointer, &itemAsPointer}, + {&itemAsPointer, &itemAsPointer}, + {&itemAsPointer, &itemAsPointer}, + }; + // rows are pointers - Row rowAsPointer = {"blue", 0x0000ff, "Blau"}; + Row rowAsPointer = {{"blue", Qt::blue, "0x0000ff"}, 0x0000ff, "Blau"}; std::vector tableOfRowPointers = { &rowAsPointer, &rowAsPointer, @@ -275,17 +347,29 @@ void tst_QGenericItemModel::createTestData() << int(std::tuple_size_v) << (ChangeAction::ChangeRows | ChangeAction::SetData); ADD_POINTER(vectorOfStructs) << int(std::tuple_size_v) << (ChangeAction::ChangeRows | ChangeAction::SetData); - ADD_COPY(vectorOfConstStructs) << int(std::tuple_size_v) << ChangeActions(ChangeAction::ChangeRows); ADD_POINTER(vectorOfConstStructs) << int(std::tuple_size_v) << ChangeActions(ChangeAction::ChangeRows); + ADD_COPY(vectorOfGadgets) + << 3 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_POINTER(vectorOfGadgets) + << 3 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_COPY(listOfGadgets) + << 1 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_POINTER(listOfGadgets) + << 1 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_COPY(listOfObjects) + << 2 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_COPY(tableOfNumbers) << 5 << ChangeActions(ChangeAction::All); ADD_POINTER(tableOfNumbers) << 5 << ChangeActions(ChangeAction::All); // only adding as pointer, copy would operate on the same data + ADD_POINTER(tableOfPointers) + << 2 << ChangeActions(ChangeAction::All); ADD_POINTER(tableOfRowPointers) << int(std::tuple_size_v) << (ChangeAction::ChangeRows | ChangeAction::SetData); @@ -598,6 +682,7 @@ void tst_QGenericItemModel::insertRows() const QVariant lastValue = lastItem.data(); QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue); QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue); + QEXPECT_FAIL("listOfObjectsCopy", "No object created", Continue); // associative containers are default constructed with no valid data ignoreFailureFromAssociativeContainers();