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]
|
||||
#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
|
||||
|
||||
\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
|
||||
|
@ -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<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);
|
||||
}
|
||||
};
|
||||
|
||||
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<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 false;
|
||||
};
|
||||
|
||||
|
@ -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 <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>
|
||||
struct test_size : std::false_type {};
|
||||
template <typename C>
|
||||
@ -82,7 +104,8 @@ namespace QGenericItemModelDetails
|
||||
};
|
||||
template <typename 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
|
||||
{
|
||||
using value_type = std::remove_reference_t<decltype(*std::begin(std::declval<C&>()))>;
|
||||
@ -182,7 +205,7 @@ namespace QGenericItemModelDetails
|
||||
|
||||
ModelStorage m_model;
|
||||
};
|
||||
} // QGenericItemModelDetails
|
||||
}
|
||||
|
||||
class QGenericItemModel;
|
||||
|
||||
|
@ -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<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;
|
||||
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user