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 <artem.dyomin@qt.io>
This commit is contained in:
parent
984074eea2
commit
c4cef19d58
@ -106,3 +106,23 @@ namespace std {
|
|||||||
}
|
}
|
||||||
//! [tuple_protocol]
|
//! [tuple_protocol]
|
||||||
#endif // __cpp_concepts && forward_like
|
#endif // __cpp_concepts && forward_like
|
||||||
|
|
||||||
|
void color_map()
|
||||||
|
{
|
||||||
|
//! [color_map]
|
||||||
|
using ColorEntry = QMap<Qt::ItemDataRole, QVariant>;
|
||||||
|
|
||||||
|
const QStringList colorNames = QColor::colorNames();
|
||||||
|
QList<ColorEntry> 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]
|
||||||
|
}
|
||||||
|
@ -91,16 +91,30 @@ QT_BEGIN_NAMESPACE
|
|||||||
|
|
||||||
\snippet qgenericitemmodel/main.cpp pair_int_QString
|
\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
|
The type of the items that the implementations of data(), setData(),
|
||||||
clearItemData() operate on can be the same across the entire model - like
|
clearItemData() etc. operate on can be the same across the entire model -
|
||||||
in the gridOfNumbers example above. But the range can also have different
|
like in the \c{gridOfNumbers} example above. But the range can also have
|
||||||
item types for different columns, like in the \c{numberNames} case.
|
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
|
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
|
roles. Most views expect the value to be
|
||||||
to and from a QString} (but a custom delegate might provide more flexibility).
|
\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
|
\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
|
Returns the data stored under the given \a role for the value in the
|
||||||
range referred to by the \a index.
|
range referred to by the \a index.
|
||||||
|
|
||||||
The implementation returns a QVariant constructed from the item at the
|
If the item type for that index is an associative container that maps from
|
||||||
\a index via \c{QVariant::fromValue()} for \c{Qt::DisplayRole} or
|
either \c{int}, Qt::ItemDataRole, or QString to a QVariant, then the role
|
||||||
\c{Qt::EditRole}. For other roles, the implementation returns an \b invalid
|
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.
|
(default-constructed) QVariant.
|
||||||
|
|
||||||
\sa Qt::ItemDataRole, setData(), headerData()
|
\sa Qt::ItemDataRole, setData(), headerData()
|
||||||
@ -275,10 +293,16 @@ QVariant QGenericItemModel::data(const QModelIndex &index, int role) const
|
|||||||
/*!
|
/*!
|
||||||
\reimp
|
\reimp
|
||||||
|
|
||||||
Sets the \a role data for the item at \a index to \a data. This
|
Sets the \a role data for the item at \a index to \a data.
|
||||||
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
|
If the item type for that \a index is an associative container that maps
|
||||||
\c{true}. For other roles, the implementation returns \c{false}.
|
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]
|
//! [read-only-setData]
|
||||||
For models operating on a read-only range, or on a read-only column in
|
For models operating on a read-only range, or on a read-only column in
|
||||||
|
@ -251,9 +251,24 @@ public:
|
|||||||
QVariant data(const QModelIndex &index, int role) const
|
QVariant data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
QVariant result;
|
QVariant result;
|
||||||
const auto readData = [&result, role](const auto &value) {
|
const auto readData = [this, &result, role](const auto &value) {
|
||||||
if (role == Qt::DisplayRole || role == Qt::EditRole)
|
Q_UNUSED(this);
|
||||||
|
using value_type = q20::remove_cvref_t<decltype(value)>;
|
||||||
|
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
|
||||||
|
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);
|
result = read(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (index.isValid()) {
|
if (index.isValid()) {
|
||||||
@ -282,9 +297,29 @@ public:
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const auto writeData = [&data, role](auto &&target) -> bool {
|
const auto writeData = [this, &data, role](auto &&target) -> bool {
|
||||||
if (role == Qt::DisplayRole || role == Qt::EditRole)
|
using value_type = q20::remove_cvref_t<decltype(target)>;
|
||||||
|
using multi_role = QGenericItemModelDetails::is_multi_role<value_type>;
|
||||||
|
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 write(target, data);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -68,6 +68,28 @@ namespace QGenericItemModelDetails
|
|||||||
: std::true_type
|
: 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 <typename C, typename = void> struct is_multi_role : std::false_type
|
||||||
|
{
|
||||||
|
static constexpr bool int_key = false;
|
||||||
|
};
|
||||||
|
template <typename C> // Qt::ItemDataRole -> QVariant, or QString -> QVariant, int -> QVariant
|
||||||
|
struct is_multi_role<C, std::void_t<typename C::key_type, typename C::mapped_type>>
|
||||||
|
: std::conjunction<std::disjunction<std::is_same<typename C::key_type, int>,
|
||||||
|
std::is_same<typename C::key_type, Qt::ItemDataRole>,
|
||||||
|
std::is_same<typename C::key_type, QString>>,
|
||||||
|
std::is_same<typename C::mapped_type, QVariant>>
|
||||||
|
{
|
||||||
|
static constexpr bool int_key = !std::is_same_v<typename C::key_type, QString>;
|
||||||
|
};
|
||||||
|
template <typename C>
|
||||||
|
[[maybe_unused]]
|
||||||
|
static constexpr bool is_multi_role_v = is_multi_role<C>::value;
|
||||||
|
|
||||||
template <typename C, typename = void>
|
template <typename C, typename = void>
|
||||||
struct test_size : std::false_type {};
|
struct test_size : std::false_type {};
|
||||||
template <typename C>
|
template <typename C>
|
||||||
@ -82,7 +104,8 @@ namespace QGenericItemModelDetails
|
|||||||
};
|
};
|
||||||
template <typename C>
|
template <typename C>
|
||||||
struct range_traits<C, std::void_t<decltype(std::cbegin(std::declval<C&>())),
|
struct range_traits<C, std::void_t<decltype(std::cbegin(std::declval<C&>())),
|
||||||
decltype(std::cend(std::declval<C&>()))
|
decltype(std::cend(std::declval<C&>())),
|
||||||
|
std::enable_if_t<!is_multi_role_v<C>>
|
||||||
>> : std::true_type
|
>> : std::true_type
|
||||||
{
|
{
|
||||||
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
|
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
|
||||||
@ -182,7 +205,7 @@ namespace QGenericItemModelDetails
|
|||||||
|
|
||||||
ModelStorage m_model;
|
ModelStorage m_model;
|
||||||
};
|
};
|
||||||
} // QGenericItemModelDetails
|
}
|
||||||
|
|
||||||
class QGenericItemModel;
|
class QGenericItemModel;
|
||||||
|
|
||||||
|
@ -169,6 +169,20 @@ private:
|
|||||||
{16.0, 17.0, 18.0, 19.0, 20.0},
|
{16.0, 17.0, 18.0, 19.0, 20.0},
|
||||||
{21.0, 22.0, 23.0, 24.0, 25.0},
|
{21.0, 22.0, 23.0, 24.0, 25.0},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// values are associative containers
|
||||||
|
std::vector<QVariantMap> listOfNamedRoles = {
|
||||||
|
{{"display", "DISPLAY0"}, {"decoration", "DECORATION0"}},
|
||||||
|
{{"display", "DISPLAY1"}, {"decoration", "DECORATION1"}},
|
||||||
|
{{"display", "DISPLAY2"}, {"decoration", "DECORATION2"}},
|
||||||
|
{{"display", "DISPLAY3"}, {"decoration", "DECORATION3"}},
|
||||||
|
};
|
||||||
|
std::vector<std::vector<std::map<Qt::ItemDataRole, QVariant>>> 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<Data> m_data;
|
std::unique_ptr<Data> m_data;
|
||||||
@ -273,6 +287,15 @@ void tst_QGenericItemModel::createTestData()
|
|||||||
ADD_POINTER(constTableOfNumbers)
|
ADD_POINTER(constTableOfNumbers)
|
||||||
<< 5 << ChangeActions(ChangeAction::ReadOnly);
|
<< 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_COPY
|
||||||
#undef ADD_POINTER
|
#undef ADD_POINTER
|
||||||
#undef ADD_HELPER
|
#undef ADD_HELPER
|
||||||
@ -467,6 +490,12 @@ void tst_QGenericItemModel::insertRows()
|
|||||||
QCOMPARE(model->rowCount() == expectedRowCount + 1,
|
QCOMPARE(model->rowCount() == expectedRowCount + 1,
|
||||||
changeActions.testFlag(ChangeAction::InsertRows));
|
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
|
// get and put data into the new row
|
||||||
const QModelIndex firstItem = model->index(0, 0);
|
const QModelIndex firstItem = model->index(0, 0);
|
||||||
const QModelIndex lastItem = model->index(0, expectedColumnCount - 1);
|
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("tableOfPointersPointer", "No item created", Continue);
|
||||||
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
|
QEXPECT_FAIL("tableOfRowPointersPointer", "No row created", Continue);
|
||||||
|
|
||||||
|
// associative containers are default constructed with no valid data
|
||||||
|
ignoreFailureFromAssociativeContainers();
|
||||||
|
|
||||||
QVERIFY(firstValue.isValid() && lastValue.isValid());
|
QVERIFY(firstValue.isValid() && lastValue.isValid());
|
||||||
|
ignoreFailureFromAssociativeContainers();
|
||||||
QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
|
QCOMPARE(model->setData(firstItem, lastValue), canSetData && lastValue.isValid());
|
||||||
|
ignoreFailureFromAssociativeContainers();
|
||||||
QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());
|
QCOMPARE(model->setData(lastItem, firstValue), canSetData && firstValue.isValid());
|
||||||
|
|
||||||
// append more rows
|
// append more rows
|
||||||
|
Loading…
x
Reference in New Issue
Block a user