From c4cef19d5827997a21e1d979646ddba576c3d1c4 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Fri, 7 Feb 2025 14:58:57 +0100 Subject: [PATCH] QGIM: support associative containers for multi-role items If the range used with QGenericItemModel contains associative containers that map from Qt::ItemDataRole, int, or QString to QVariant, then we interpret such values as multi-role items, and look up the respective value for the role requested. Add test coverage and documentation. This change does not implement QAbstractItemModel::itemData/setItemData. Change-Id: I00aa8da9369a20ea4ddb58bb24c45fef9465b33f Reviewed-by: Artem Dyomin --- .../doc/snippets/qgenericitemmodel/main.cpp | 20 +++++++ src/corelib/itemmodels/qgenericitemmodel.cpp | 52 ++++++++++++++----- src/corelib/itemmodels/qgenericitemmodel.h | 43 +++++++++++++-- .../itemmodels/qgenericitemmodel_impl.h | 27 +++++++++- .../tst_qgenericitemmodel.cpp | 34 ++++++++++++ 5 files changed, 156 insertions(+), 20 deletions(-) diff --git a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp index b99c600684c..9251d3c1f0e 100644 --- a/src/corelib/doc/snippets/qgenericitemmodel/main.cpp +++ b/src/corelib/doc/snippets/qgenericitemmodel/main.cpp @@ -106,3 +106,23 @@ namespace std { } //! [tuple_protocol] #endif // __cpp_concepts && forward_like + +void color_map() +{ +//! [color_map] +using ColorEntry = QMap; + +const QStringList colorNames = QColor::colorNames(); +QList colors; +colors.reserve(colorNames.size()); +for (const QString &name : colorNames) { + const QColor color = QColor::fromString(name); + colors << ColorEntry{{Qt::DisplayRole, name}, + {Qt::DecorationRole, color}, + {Qt::ToolTipRole, color.name()}}; +} +QGenericItemModel colorModel(colors); +QListView list; +list.setModel(&colorModel); +//! [color_map] +} diff --git a/src/corelib/itemmodels/qgenericitemmodel.cpp b/src/corelib/itemmodels/qgenericitemmodel.cpp index 9eb0983cccf..4df7fab1d49 100644 --- a/src/corelib/itemmodels/qgenericitemmodel.cpp +++ b/src/corelib/itemmodels/qgenericitemmodel.cpp @@ -91,16 +91,30 @@ QT_BEGIN_NAMESPACE \snippet qgenericitemmodel/main.cpp pair_int_QString - \section2 Item Types + \section2 Multi-role items - The type of the items that the implementations of data(), setData(), and - clearItemData() operate on can be the same across the entire model - like - in the gridOfNumbers example above. But the range can also have different - item types for different columns, like in the \c{numberNames} case. + The type of the items that the implementations of data(), setData(), + clearItemData() etc. operate on can be the same across the entire model - + like in the \c{gridOfNumbers} example above. But the range can also have + different item types for different columns, like in the \c{numberNames} + case. By default, the value gets used for the Qt::DisplayRole and Qt::EditRole - roles. Most views expect the value to be \l{QVariant::canConvert}{convertible - to and from a QString} (but a custom delegate might provide more flexibility). + roles. Most views expect the value to be + \l{QVariant::canConvert}{convertible to and from a QString} (but a custom + delegate might provide more flexibility). + + If the item is an associative container that uses \c{int}, + \l{Qt::ItemDataRole}, or QString as the key type, and QVariant as the + mapped type, then QGenericItemModel interprets that container as the storage + of the data for multiple roles. The data() and setData() functions return + and modify the mapped value in the container, and clearItemData() clears + the entire container. + + \snippet qgenericitemmodel/main.cpp color_map + + The most efficient data type to use as the key is Qt::ItemDataRole or + \c{int}. \section2 The C++ tuple protocol @@ -260,9 +274,13 @@ QVariant QGenericItemModel::headerData(int section, Qt::Orientation orientation, Returns the data stored under the given \a role for the value in the range referred to by the \a index. - The implementation returns a QVariant constructed from the item at the - \a index via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or - \c{Qt::EditRole}. For other roles, the implementation returns an \b invalid + If the item type for that index is an associative container that maps from + either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then the role + data is looked up in that container and returned. + + Otherwise, the implementation returns a QVariant constructed from the item + via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or \c{Qt::EditRole}. + For other roles, the implementation returns an \b invalid (default-constructed) QVariant. \sa Qt::ItemDataRole, setData(), headerData() @@ -275,10 +293,16 @@ QVariant QGenericItemModel::data(const QModelIndex &index, int role) const /*! \reimp - Sets the \a role data for the item at \a index to \a data. This - implementation assigns the value in \a data to the item at the \a index - in the range for \c{Qt::DisplayRole} and \c{Qt::EditRole}, and returns - \c{true}. For other roles, the implementation returns \c{false}. + Sets the \a role data for the item at \a index to \a data. + + If the item type for that \a index is an associative container that maps + from either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then + \a data is stored in that container for the key specified by \a role. + + Otherwise, this implementation assigns the value in \a data to the item at + the \a index in the range for \c{Qt::DisplayRole} and \c{Qt::EditRole}, + and returns \c{true}. For other roles, the implementation returns + \c{false}. //! [read-only-setData] For models operating on a read-only range, or on a read-only column in diff --git a/src/corelib/itemmodels/qgenericitemmodel.h b/src/corelib/itemmodels/qgenericitemmodel.h index 7667164a088..0f62bd966cd 100644 --- a/src/corelib/itemmodels/qgenericitemmodel.h +++ b/src/corelib/itemmodels/qgenericitemmodel.h @@ -251,9 +251,24 @@ public: QVariant data(const QModelIndex &index, int role) const { QVariant result; - const auto readData = [&result, role](const auto &value) { - if (role == Qt::DisplayRole || role == Qt::EditRole) + const auto readData = [this, &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) { + const auto it = [this, &value, role]{ + Q_UNUSED(this); + if constexpr (multi_role::int_key) + return std::as_const(value).find(Qt::ItemDataRole(role)); + else + return std::as_const(value).find(roleNames().value(role)); + }(); + if (it != value.cend()) { + result = QGenericItemModelDetails::value(it); + } + } else if (role == Qt::DisplayRole || role == Qt::EditRole) { result = read(value); + } }; if (index.isValid()) { @@ -282,9 +297,29 @@ public: } }); - const auto writeData = [&data, role](auto &&target) -> bool { - if (role == Qt::DisplayRole || role == Qt::EditRole) + const auto writeData = [this, &data, role](auto &&target) -> bool { + using value_type = q20::remove_cvref_t; + using multi_role = QGenericItemModelDetails::is_multi_role; + 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. + if (role == Qt::EditRole) { + if constexpr (multi_role::int_key) { + if (target.find(roleToSet) == target.end()) + roleToSet = Qt::DisplayRole; + } else { + if (target.find(roleNames().value(roleToSet)) == target.end()) + roleToSet = Qt::DisplayRole; + } + } + if constexpr (multi_role::int_key) + return write(target[roleToSet], data); + else + return write(target[roleNames().value(roleToSet)], data); + } else if (role == Qt::DisplayRole || role == Qt::EditRole) { return write(target, data); + } return false; }; diff --git a/src/corelib/itemmodels/qgenericitemmodel_impl.h b/src/corelib/itemmodels/qgenericitemmodel_impl.h index 9b03be109f1..a017f5c94e6 100644 --- a/src/corelib/itemmodels/qgenericitemmodel_impl.h +++ b/src/corelib/itemmodels/qgenericitemmodel_impl.h @@ -68,6 +68,28 @@ namespace QGenericItemModelDetails : std::true_type {}; + // Test if a type is an associative container that we can use for multi-role + // data, i.e. has a key_type and a mapped_type typedef, and maps from int, + // Qt::ItemDataRole, or QString to QVariant. This excludes std::set (and + // unordered_set), which are not useful for us anyway even though they are + // considered associative containers. + template struct is_multi_role : std::false_type + { + static constexpr bool int_key = false; + }; + template // Qt::ItemDataRole -> QVariant, or QString -> QVariant, int -> QVariant + struct is_multi_role> + : std::conjunction, + std::is_same, + std::is_same>, + std::is_same> + { + static constexpr bool int_key = !std::is_same_v; + }; + template + [[maybe_unused]] + static constexpr bool is_multi_role_v = is_multi_role::value; + template struct test_size : std::false_type {}; template @@ -82,7 +104,8 @@ namespace QGenericItemModelDetails }; template struct range_traits())), - decltype(std::cend(std::declval())) + decltype(std::cend(std::declval())), + std::enable_if_t> >> : std::true_type { using value_type = std::remove_reference_t()))>; @@ -182,7 +205,7 @@ namespace QGenericItemModelDetails ModelStorage m_model; }; -} // QGenericItemModelDetails +} class QGenericItemModel; diff --git a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp index f545f54d8ee..228924386d0 100644 --- a/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp +++ b/tests/auto/corelib/itemmodels/qgenericitemmodel/tst_qgenericitemmodel.cpp @@ -169,6 +169,20 @@ private: {16.0, 17.0, 18.0, 19.0, 20.0}, {21.0, 22.0, 23.0, 24.0, 25.0}, }; + + // values are associative containers + std::vector listOfNamedRoles = { + {{"display", "DISPLAY0"}, {"decoration", "DECORATION0"}}, + {{"display", "DISPLAY1"}, {"decoration", "DECORATION1"}}, + {{"display", "DISPLAY2"}, {"decoration", "DECORATION2"}}, + {{"display", "DISPLAY3"}, {"decoration", "DECORATION3"}}, + }; + std::vector>> tableOfEnumRoles = { + {{{Qt::DisplayRole, "DISPLAY0"}, {Qt::DecorationRole, "DECORATION0"}}}, + {{{Qt::DisplayRole, "DISPLAY1"}, {Qt::DecorationRole, "DECORATION1"}}}, + {{{Qt::DisplayRole, "DISPLAY2"}, {Qt::DecorationRole, "DECORATION2"}}}, + {{{Qt::DisplayRole, "DISPLAY3"}, {Qt::DecorationRole, "DECORATION3"}}}, + }; }; std::unique_ptr m_data; @@ -273,6 +287,15 @@ void tst_QGenericItemModel::createTestData() ADD_POINTER(constTableOfNumbers) << 5 << ChangeActions(ChangeAction::ReadOnly); + ADD_COPY(listOfNamedRoles) + << 1 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_POINTER(listOfNamedRoles) + << 1 << (ChangeAction::ChangeRows | ChangeAction::SetData); + ADD_COPY(tableOfEnumRoles) + << 1 << ChangeActions(ChangeAction::All); + ADD_POINTER(tableOfEnumRoles) + << 1 << ChangeActions(ChangeAction::All); + #undef ADD_COPY #undef ADD_POINTER #undef ADD_HELPER @@ -467,6 +490,12 @@ void tst_QGenericItemModel::insertRows() QCOMPARE(model->rowCount() == expectedRowCount + 1, changeActions.testFlag(ChangeAction::InsertRows)); + auto ignoreFailureFromAssociativeContainers = []{ + QEXPECT_FAIL("listOfNamedRolesPointer", "QVariantMap is empty by design", Continue); + QEXPECT_FAIL("listOfNamedRolesCopy", "QVariantMap is empty by design", Continue); + QEXPECT_FAIL("tableOfEnumRolesPointer", "QVariantMap is empty by design", Continue); + QEXPECT_FAIL("tableOfEnumRolesCopy", "QVariantMap is empty by design", Continue); + }; // get and put data into the new row const QModelIndex firstItem = model->index(0, 0); const QModelIndex lastItem = model->index(0, expectedColumnCount - 1); @@ -477,8 +506,13 @@ void tst_QGenericItemModel::insertRows() QEXPECT_FAIL("tableOfPointersPointer", "No item created", Continue); QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue); + // associative containers are default constructed with no valid data + ignoreFailureFromAssociativeContainers(); + QVERIFY(firstValue.isValid() && lastValue.isValid()); + ignoreFailureFromAssociativeContainers(); QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid()); + ignoreFailureFromAssociativeContainers(); QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid()); // append more rows