New proxy model: QConcatenateTablesProxyModel
It takes multiple source models and concatenates their rows into a single model. With full unit tests. [ChangeLog][QtCore] New class QConcatenateTablesProxyModel, to concatenate the rows from multiple source models. Change-Id: Iaf4f325473adef106f423677fdc5ee0e35e87d35 Reviewed-by: Luca Beldi <v.ronin@yahoo.it> Reviewed-by: Sérgio Martins <sergio.martins@kdab.com>
This commit is contained in:
parent
be27bf02f4
commit
c82ab86cea
@ -793,6 +793,13 @@
|
||||
"condition": "features.proxymodel",
|
||||
"output": [ "publicFeature", "feature" ]
|
||||
},
|
||||
"concatenatetablesproxymodel": {
|
||||
"label": "QConcatenateTablesProxyModel",
|
||||
"purpose": "Supports concatenating source models.",
|
||||
"section": "ItemViews",
|
||||
"condition": "features.proxymodel",
|
||||
"output": [ "publicFeature", "feature" ]
|
||||
},
|
||||
"stringlistmodel": {
|
||||
"label": "QStringListModel",
|
||||
"purpose": "Provides a model that supplies strings to views.",
|
||||
|
@ -20,6 +20,14 @@ qtConfig(proxymodel) {
|
||||
SOURCES += \
|
||||
itemmodels/qabstractproxymodel.cpp
|
||||
|
||||
qtConfig(concatenatetablesproxymodel) {
|
||||
HEADERS += \
|
||||
itemmodels/qconcatenatetablesproxymodel.h
|
||||
|
||||
SOURCES += \
|
||||
itemmodels/qconcatenatetablesproxymodel.cpp
|
||||
}
|
||||
|
||||
qtConfig(identityproxymodel) {
|
||||
HEADERS += \
|
||||
itemmodels/qidentityproxymodel.h
|
||||
|
750
src/corelib/itemmodels/qconcatenatetablesproxymodel.cpp
Normal file
750
src/corelib/itemmodels/qconcatenatetablesproxymodel.cpp
Normal file
@ -0,0 +1,750 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtCore module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include "qconcatenatetablesproxymodel.h"
|
||||
#include <private/qabstractitemmodel_p.h>
|
||||
#include "qsize.h"
|
||||
#include "qdebug.h"
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QConcatenateTablesProxyModelPrivate : public QAbstractItemModelPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(QConcatenateTablesProxyModel);
|
||||
|
||||
public:
|
||||
QConcatenateTablesProxyModelPrivate();
|
||||
|
||||
int computeRowsPrior(const QAbstractItemModel *sourceModel) const;
|
||||
|
||||
struct SourceModelForRowResult
|
||||
{
|
||||
SourceModelForRowResult() : sourceModel(Q_NULLPTR), sourceRow(-1) {}
|
||||
QAbstractItemModel *sourceModel;
|
||||
int sourceRow;
|
||||
};
|
||||
SourceModelForRowResult sourceModelForRow(int row) const;
|
||||
|
||||
void _q_slotRowsAboutToBeInserted(const QModelIndex &, int start, int end);
|
||||
void _q_slotRowsInserted(const QModelIndex &, int start, int end);
|
||||
void _q_slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end);
|
||||
void _q_slotRowsRemoved(const QModelIndex &, int start, int end);
|
||||
void _q_slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end);
|
||||
void _q_slotColumnsInserted(const QModelIndex &parent, int, int);
|
||||
void _q_slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
|
||||
void _q_slotColumnsRemoved(const QModelIndex &parent, int, int);
|
||||
void _q_slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles);
|
||||
void _q_slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
|
||||
void _q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint);
|
||||
void _q_slotModelAboutToBeReset();
|
||||
void _q_slotModelReset();
|
||||
int columnCountAfterChange(const QAbstractItemModel *model, int newCount) const;
|
||||
int calculatedColumnCount() const;
|
||||
void updateColumnCount();
|
||||
bool mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent,
|
||||
int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const;
|
||||
|
||||
QVector<QAbstractItemModel *> m_models;
|
||||
int m_rowCount; // have to maintain it here since we can't compute during model destruction
|
||||
int m_columnCount;
|
||||
|
||||
// for columns{AboutToBe,}{Inserted,Removed}
|
||||
int m_newColumnCount;
|
||||
|
||||
// for layoutAboutToBeChanged/layoutChanged
|
||||
QVector<QPersistentModelIndex> layoutChangePersistentIndexes;
|
||||
QVector<QModelIndex> layoutChangeProxyIndexes;
|
||||
};
|
||||
|
||||
QConcatenateTablesProxyModelPrivate::QConcatenateTablesProxyModelPrivate()
|
||||
: m_rowCount(0),
|
||||
m_columnCount(0),
|
||||
m_newColumnCount(0)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
\since 5.13
|
||||
\class QConcatenateTablesProxyModel
|
||||
\inmodule QtCore
|
||||
\brief The QConcatenateTablesProxyModel class proxies multiple source models, concatenating their rows
|
||||
|
||||
\ingroup model-view
|
||||
|
||||
QConcatenateTablesProxyModel takes multiple source models and concatenates their rows.
|
||||
|
||||
In other words, the proxy will have all rows of the first source model,
|
||||
followed by all rows of the second source model, and so on.
|
||||
|
||||
If the source models don't have the same number of columns, the proxy will only
|
||||
have as many columns as the source model with the smallest number of columns.
|
||||
Additional columns in other source models will simply be ignored.
|
||||
|
||||
Source models can be added and removed at runtime, and the column count is adjusted accordingly.
|
||||
|
||||
This proxy does not inherit from QAbstractProxyModel because it uses multiple source
|
||||
models, rather than a single one.
|
||||
|
||||
Only flat models (lists and tables) are supported, tree models are not.
|
||||
|
||||
\sa QAbstractProxyModel, {Model/View Programming}, QIdentityProxyModel, QAbstractItemModel
|
||||
*/
|
||||
|
||||
|
||||
/*!
|
||||
Constructs a concatenate-rows proxy model with the given \a parent.
|
||||
*/
|
||||
QConcatenateTablesProxyModel::QConcatenateTablesProxyModel(QObject *parent)
|
||||
: QAbstractItemModel(*new QConcatenateTablesProxyModelPrivate, parent)
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
Destroys this proxy model.
|
||||
*/
|
||||
QConcatenateTablesProxyModel::~QConcatenateTablesProxyModel()
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the proxy index for a given \a sourceIndex, which can be from any of the source models.
|
||||
*/
|
||||
QModelIndex QConcatenateTablesProxyModel::mapFromSource(const QModelIndex &sourceIndex) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (!sourceIndex.isValid())
|
||||
return QModelIndex();
|
||||
const QAbstractItemModel *sourceModel = sourceIndex.model();
|
||||
if (!d->m_models.contains(const_cast<QAbstractItemModel *>(sourceModel))) {
|
||||
qWarning("QConcatenateTablesProxyModel: index from wrong model passed to mapFromSource");
|
||||
Q_ASSERT(!"QConcatenateTablesProxyModel: index from wrong model passed to mapFromSource");
|
||||
return QModelIndex();
|
||||
}
|
||||
if (sourceIndex.column() >= d->m_columnCount)
|
||||
return QModelIndex();
|
||||
int rowsPrior = d_func()->computeRowsPrior(sourceModel);
|
||||
return createIndex(rowsPrior + sourceIndex.row(), sourceIndex.column(), sourceIndex.internalPointer());
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the source index for a given proxy index.
|
||||
*/
|
||||
QModelIndex QConcatenateTablesProxyModel::mapToSource(const QModelIndex &proxyIndex) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(checkIndex(proxyIndex));
|
||||
if (!proxyIndex.isValid())
|
||||
return QModelIndex();
|
||||
if (proxyIndex.model() != this) {
|
||||
qWarning("QConcatenateTablesProxyModel: index from wrong model passed to mapToSource");
|
||||
Q_ASSERT(!"QConcatenateTablesProxyModel: index from wrong model passed to mapToSource");
|
||||
return QModelIndex();
|
||||
}
|
||||
const int row = proxyIndex.row();
|
||||
const auto result = d->sourceModelForRow(row);
|
||||
if (!result.sourceModel)
|
||||
return QModelIndex();
|
||||
return result.sourceModel->index(result.sourceRow, proxyIndex.column());
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
QVariant QConcatenateTablesProxyModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid));
|
||||
if (!sourceIndex.isValid())
|
||||
return QVariant();
|
||||
return sourceIndex.data(role);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
bool QConcatenateTablesProxyModel::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
Q_ASSERT(checkIndex(index, CheckIndexOption::IndexIsValid));
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
Q_ASSERT(sourceIndex.isValid());
|
||||
const auto sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
|
||||
return sourceModel->setData(sourceIndex, value, role);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
QMap<int, QVariant> QConcatenateTablesProxyModel::itemData(const QModelIndex &proxyIndex) const
|
||||
{
|
||||
Q_ASSERT(checkIndex(proxyIndex));
|
||||
const QModelIndex sourceIndex = mapToSource(proxyIndex);
|
||||
Q_ASSERT(sourceIndex.isValid());
|
||||
return sourceIndex.model()->itemData(sourceIndex);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
bool QConcatenateTablesProxyModel::setItemData(const QModelIndex &proxyIndex, const QMap<int, QVariant> &roles)
|
||||
{
|
||||
Q_ASSERT(checkIndex(proxyIndex));
|
||||
const QModelIndex sourceIndex = mapToSource(proxyIndex);
|
||||
Q_ASSERT(sourceIndex.isValid());
|
||||
const auto sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model());
|
||||
return sourceModel->setItemData(sourceIndex, roles);
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the flags for the given index.
|
||||
If the index is valid, the flags come from the source model for this index.
|
||||
If the index is invalid (as used to determine if dropping onto an empty area
|
||||
in the view is allowed, for instance), the flags from the first model are returned.
|
||||
*/
|
||||
Qt::ItemFlags QConcatenateTablesProxyModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (d->m_models.isEmpty())
|
||||
return Qt::NoItemFlags;
|
||||
Q_ASSERT(checkIndex(index));
|
||||
if (!index.isValid())
|
||||
return d->m_models.at(0)->flags(index);
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
Q_ASSERT(sourceIndex.isValid());
|
||||
return sourceIndex.model()->flags(sourceIndex);
|
||||
}
|
||||
|
||||
/*!
|
||||
This method returns the horizontal header data for the first source model,
|
||||
and the vertical header data for the source model corresponding to each row.
|
||||
\reimp
|
||||
*/
|
||||
QVariant QConcatenateTablesProxyModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (d->m_models.isEmpty())
|
||||
return QVariant();
|
||||
switch (orientation) {
|
||||
case Qt::Horizontal:
|
||||
return d->m_models.at(0)->headerData(section, orientation, role);
|
||||
case Qt::Vertical: {
|
||||
const auto result = d->sourceModelForRow(section);
|
||||
Q_ASSERT(result.sourceModel);
|
||||
return result.sourceModel->headerData(result.sourceRow, orientation, role);
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
/*!
|
||||
This method returns the column count of the source model with the smallest number of columns.
|
||||
\reimp
|
||||
*/
|
||||
int QConcatenateTablesProxyModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (parent.isValid())
|
||||
return 0; // flat model
|
||||
return d->m_columnCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
QModelIndex QConcatenateTablesProxyModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(hasIndex(row, column, parent));
|
||||
if (!hasIndex(row, column, parent))
|
||||
return QModelIndex();
|
||||
Q_ASSERT(checkIndex(parent, QAbstractItemModel::CheckIndexOption::ParentIsInvalid)); // flat model
|
||||
const auto result = d->sourceModelForRow(row);
|
||||
Q_ASSERT(result.sourceModel);
|
||||
return mapFromSource(result.sourceModel->index(result.sourceRow, column));
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
QModelIndex QConcatenateTablesProxyModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
return QModelIndex(); // flat model, no hierarchy
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
int QConcatenateTablesProxyModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(checkIndex(parent, QAbstractItemModel::CheckIndexOption::ParentIsInvalid)); // flat model
|
||||
Q_UNUSED(parent);
|
||||
return d->m_rowCount;
|
||||
}
|
||||
|
||||
/*!
|
||||
This method returns the mime types for the first source model.
|
||||
\reimp
|
||||
*/
|
||||
QStringList QConcatenateTablesProxyModel::mimeTypes() const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (d->m_models.isEmpty())
|
||||
return QStringList();
|
||||
return d->m_models.at(0)->mimeTypes();
|
||||
}
|
||||
|
||||
/*!
|
||||
The call is forwarded to the source model of the first index in the list of \a indexes.
|
||||
|
||||
Important: please note that this proxy only supports dragging a single row.
|
||||
It will assert if called with indexes from multiple rows, because dragging rows that
|
||||
might come from different source models cannot be implemented generically by this proxy model.
|
||||
Each piece of data in the QMimeData needs to be merged, which is data-type-specific.
|
||||
Reimplement this method in a subclass if you want to support dragging multiple rows.
|
||||
|
||||
\reimp
|
||||
*/
|
||||
QMimeData *QConcatenateTablesProxyModel::mimeData(const QModelIndexList &indexes) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (indexes.isEmpty())
|
||||
return nullptr;
|
||||
const QModelIndex firstIndex = indexes.first();
|
||||
Q_ASSERT(checkIndex(firstIndex, CheckIndexOption::IndexIsValid));
|
||||
const auto result = d->sourceModelForRow(firstIndex.row());
|
||||
QModelIndexList sourceIndexes;
|
||||
sourceIndexes.reserve(indexes.count());
|
||||
for (const QModelIndex &index : indexes) {
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
Q_ASSERT(sourceIndex.model() == result.sourceModel); // see documentation above
|
||||
sourceIndexes.append(sourceIndex);
|
||||
}
|
||||
return result.sourceModel->mimeData(sourceIndexes);
|
||||
}
|
||||
|
||||
|
||||
bool QConcatenateTablesProxyModelPrivate::mapDropCoordinatesToSource(int row, int column, const QModelIndex &parent,
|
||||
int *sourceRow, int *sourceColumn, QModelIndex *sourceParent, QAbstractItemModel **sourceModel) const
|
||||
{
|
||||
Q_Q(const QConcatenateTablesProxyModel);
|
||||
*sourceColumn = column;
|
||||
if (!parent.isValid()) {
|
||||
// Drop after the last item
|
||||
if (row == -1 || row == m_rowCount) {
|
||||
*sourceRow = -1;
|
||||
*sourceModel = m_models.constLast();
|
||||
return true;
|
||||
}
|
||||
// Drop between toplevel items
|
||||
const auto result = sourceModelForRow(row);
|
||||
Q_ASSERT(result.sourceModel);
|
||||
*sourceRow = result.sourceRow;
|
||||
*sourceModel = result.sourceModel;
|
||||
return true;
|
||||
} else {
|
||||
if (row > -1)
|
||||
return false; // flat model, no dropping as new children of items
|
||||
// Drop onto item
|
||||
const int targetRow = parent.row();
|
||||
const auto result = sourceModelForRow(targetRow);
|
||||
Q_ASSERT(result.sourceModel);
|
||||
const QModelIndex sourceIndex = q->mapToSource(parent);
|
||||
*sourceRow = -1;
|
||||
*sourceParent = sourceIndex;
|
||||
*sourceModel = result.sourceModel;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
bool QConcatenateTablesProxyModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (d->m_models.isEmpty())
|
||||
return false;
|
||||
|
||||
int sourceRow, sourceColumn;
|
||||
QModelIndex sourceParent;
|
||||
QAbstractItemModel *sourceModel;
|
||||
if (!d->mapDropCoordinatesToSource(row, column, parent, &sourceRow, &sourceColumn, &sourceParent, &sourceModel))
|
||||
return false;
|
||||
return sourceModel->canDropMimeData(data, action, sourceRow, sourceColumn, sourceParent);
|
||||
}
|
||||
|
||||
/*!
|
||||
QConcatenateTablesProxyModel handles dropping onto an item, between items, and after the last item.
|
||||
In all cases the call is forwarded to the underlying source model.
|
||||
When dropping onto an item, the source model for this item is called.
|
||||
When dropping between items, the source model immediately below the drop position is called.
|
||||
When dropping after the last item, the last source model is called.
|
||||
|
||||
\reimp
|
||||
*/
|
||||
bool QConcatenateTablesProxyModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
if (d->m_models.isEmpty())
|
||||
return false;
|
||||
int sourceRow, sourceColumn;
|
||||
QModelIndex sourceParent;
|
||||
QAbstractItemModel *sourceModel;
|
||||
if (!d->mapDropCoordinatesToSource(row, column, parent, &sourceRow, &sourceColumn, &sourceParent, &sourceModel))
|
||||
return false;
|
||||
|
||||
return sourceModel->dropMimeData(data, action, sourceRow, sourceColumn, sourceParent);
|
||||
}
|
||||
|
||||
/*!
|
||||
\reimp
|
||||
*/
|
||||
QSize QConcatenateTablesProxyModel::span(const QModelIndex &index) const
|
||||
{
|
||||
Q_D(const QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(checkIndex(index));
|
||||
if (d->m_models.isEmpty() || !index.isValid())
|
||||
return QSize();
|
||||
const QModelIndex sourceIndex = mapToSource(index);
|
||||
Q_ASSERT(sourceIndex.isValid());
|
||||
return sourceIndex.model()->span(sourceIndex);
|
||||
}
|
||||
|
||||
/*!
|
||||
Adds a source model \a sourceModel, below all previously added source models.
|
||||
|
||||
The ownership of \a sourceModel is not affected by this.
|
||||
|
||||
The same source model cannot be added more than once.
|
||||
*/
|
||||
void QConcatenateTablesProxyModel::addSourceModel(QAbstractItemModel *sourceModel)
|
||||
{
|
||||
Q_D(QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(sourceModel);
|
||||
Q_ASSERT(!d->m_models.contains(sourceModel));
|
||||
connect(sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), this, SLOT(_q_slotDataChanged(QModelIndex,QModelIndex,QVector<int>)));
|
||||
connect(sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(_q_slotRowsInserted(QModelIndex,int,int)));
|
||||
connect(sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(_q_slotRowsRemoved(QModelIndex,int,int)));
|
||||
connect(sourceModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(_q_slotRowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
connect(sourceModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(_q_slotRowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
|
||||
connect(sourceModel, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(_q_slotColumnsInserted(QModelIndex,int,int)));
|
||||
connect(sourceModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(_q_slotColumnsRemoved(QModelIndex,int,int)));
|
||||
connect(sourceModel, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(_q_slotColumnsAboutToBeInserted(QModelIndex,int,int)));
|
||||
connect(sourceModel, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(_q_slotColumnsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
|
||||
connect(sourceModel, SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
|
||||
this, SLOT(_q_slotSourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
|
||||
connect(sourceModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)),
|
||||
this, SLOT(_q_slotSourceLayoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)));
|
||||
connect(sourceModel, SIGNAL(modelAboutToBeReset()), this, SLOT(_q_slotModelAboutToBeReset()));
|
||||
connect(sourceModel, SIGNAL(modelReset()), this, SLOT(_q_slotModelReset()));
|
||||
|
||||
const int newRows = sourceModel->rowCount();
|
||||
if (newRows > 0)
|
||||
beginInsertRows(QModelIndex(), d->m_rowCount, d->m_rowCount + newRows - 1);
|
||||
d->m_rowCount += newRows;
|
||||
d->m_models.append(sourceModel);
|
||||
if (newRows > 0)
|
||||
endInsertRows();
|
||||
|
||||
d->updateColumnCount();
|
||||
}
|
||||
|
||||
/*!
|
||||
Removes the source model \a sourceModel, which was previously added to this proxy.
|
||||
|
||||
The ownership of \a sourceModel is not affected by this.
|
||||
*/
|
||||
void QConcatenateTablesProxyModel::removeSourceModel(QAbstractItemModel *sourceModel)
|
||||
{
|
||||
Q_D(QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(d->m_models.contains(sourceModel));
|
||||
disconnect(sourceModel, 0, this, 0);
|
||||
|
||||
const int rowsRemoved = sourceModel->rowCount();
|
||||
const int rowsPrior = d->computeRowsPrior(sourceModel); // location of removed section
|
||||
|
||||
if (rowsRemoved > 0)
|
||||
beginRemoveRows(QModelIndex(), rowsPrior, rowsPrior + rowsRemoved - 1);
|
||||
d->m_models.removeOne(sourceModel);
|
||||
d->m_rowCount -= rowsRemoved;
|
||||
if (rowsRemoved > 0)
|
||||
endRemoveRows();
|
||||
|
||||
d->updateColumnCount();
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // not supported, the proxy is a flat model
|
||||
return;
|
||||
const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
|
||||
const int rowsPrior = computeRowsPrior(model);
|
||||
q->beginInsertRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotRowsInserted(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
m_rowCount += end - start + 1;
|
||||
q->endInsertRows();
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
|
||||
const int rowsPrior = computeRowsPrior(model);
|
||||
q->beginRemoveRows(QModelIndex(), rowsPrior + start, rowsPrior + end);
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotRowsRemoved(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
m_rowCount -= end - start + 1;
|
||||
q->endRemoveRows();
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
|
||||
const int oldColCount = model->columnCount();
|
||||
const int newColCount = columnCountAfterChange(model, oldColCount + end - start + 1);
|
||||
Q_ASSERT(newColCount >= oldColCount);
|
||||
if (newColCount > oldColCount)
|
||||
// If the underlying models have a different number of columns (example: 2 and 3), inserting 2 columns in
|
||||
// the first model leads to inserting only one column in the proxy, since qMin(2+2,3) == 3.
|
||||
q->beginInsertColumns(QModelIndex(), start, qMin(end, start + newColCount - oldColCount - 1));
|
||||
m_newColumnCount = newColCount;
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotColumnsInserted(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_UNUSED(start);
|
||||
Q_UNUSED(end);
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
if (m_newColumnCount != m_columnCount) {
|
||||
m_columnCount = m_newColumnCount;
|
||||
q->endInsertColumns();
|
||||
}
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
const QAbstractItemModel * const model = static_cast<QAbstractItemModel *>(q->sender());
|
||||
const int oldColCount = model->columnCount();
|
||||
const int newColCount = columnCountAfterChange(model, oldColCount - (end - start + 1));
|
||||
Q_ASSERT(newColCount <= oldColCount);
|
||||
if (newColCount < oldColCount)
|
||||
q->beginRemoveColumns(QModelIndex(), start, qMax(end, start + oldColCount - newColCount - 1));
|
||||
m_newColumnCount = newColCount;
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotColumnsRemoved(const QModelIndex &parent, int start, int end)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
Q_UNUSED(start);
|
||||
Q_UNUSED(end);
|
||||
if (parent.isValid()) // flat model
|
||||
return;
|
||||
if (m_newColumnCount != m_columnCount) {
|
||||
m_columnCount = m_newColumnCount;
|
||||
q->endRemoveColumns();
|
||||
}
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(from.isValid());
|
||||
Q_ASSERT(to.isValid());
|
||||
const QModelIndex myFrom = q->mapFromSource(from);
|
||||
Q_ASSERT(q->checkIndex(myFrom, QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
const QModelIndex myTo = q->mapFromSource(to);
|
||||
Q_ASSERT(q->checkIndex(myTo, QAbstractItemModel::CheckIndexOption::IndexIsValid));
|
||||
emit q->dataChanged(myFrom, myTo, roles);
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
|
||||
if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
|
||||
return;
|
||||
|
||||
emit q->layoutAboutToBeChanged({}, hint);
|
||||
|
||||
const QModelIndexList persistentIndexList = q->persistentIndexList();
|
||||
layoutChangePersistentIndexes.reserve(persistentIndexList.size());
|
||||
layoutChangeProxyIndexes.reserve(persistentIndexList.size());
|
||||
|
||||
for (const QPersistentModelIndex &proxyPersistentIndex : persistentIndexList) {
|
||||
layoutChangeProxyIndexes.append(proxyPersistentIndex);
|
||||
Q_ASSERT(proxyPersistentIndex.isValid());
|
||||
const QPersistentModelIndex srcPersistentIndex = q->mapToSource(proxyPersistentIndex);
|
||||
Q_ASSERT(srcPersistentIndex.isValid());
|
||||
layoutChangePersistentIndexes << srcPersistentIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex()))
|
||||
return;
|
||||
for (int i = 0; i < layoutChangeProxyIndexes.size(); ++i) {
|
||||
const QModelIndex proxyIdx = layoutChangeProxyIndexes.at(i);
|
||||
const QModelIndex newProxyIdx = q->mapFromSource(layoutChangePersistentIndexes.at(i));
|
||||
q->changePersistentIndex(proxyIdx, newProxyIdx);
|
||||
}
|
||||
|
||||
layoutChangePersistentIndexes.clear();
|
||||
layoutChangeProxyIndexes.clear();
|
||||
|
||||
emit q->layoutChanged({}, hint);
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotModelAboutToBeReset()
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(static_cast<const QAbstractItemModel *>(q->sender()))));
|
||||
q->beginResetModel();
|
||||
// A reset might reduce both rowCount and columnCount, and we can't notify of both at the same time,
|
||||
// and notifying of one after the other leaves an intermediary invalid situation.
|
||||
// So the only safe choice is to forward it as a full reset.
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::_q_slotModelReset()
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(static_cast<const QAbstractItemModel *>(q->sender()))));
|
||||
m_columnCount = calculatedColumnCount();
|
||||
m_rowCount = computeRowsPrior(nullptr);
|
||||
q->endResetModel();
|
||||
}
|
||||
|
||||
int QConcatenateTablesProxyModelPrivate::calculatedColumnCount() const
|
||||
{
|
||||
if (m_models.isEmpty())
|
||||
return 0;
|
||||
|
||||
const auto it = std::min_element(m_models.begin(), m_models.end(), [](const QAbstractItemModel* model1, const QAbstractItemModel* model2) {
|
||||
return model1->columnCount() < model2->columnCount();
|
||||
});
|
||||
return (*it)->columnCount();
|
||||
}
|
||||
|
||||
void QConcatenateTablesProxyModelPrivate::updateColumnCount()
|
||||
{
|
||||
Q_Q(QConcatenateTablesProxyModel);
|
||||
const int newColumnCount = calculatedColumnCount();
|
||||
const int columnDiff = newColumnCount - m_columnCount;
|
||||
if (columnDiff > 0) {
|
||||
q->beginInsertColumns(QModelIndex(), m_columnCount, m_columnCount + columnDiff - 1);
|
||||
m_columnCount = newColumnCount;
|
||||
q->endInsertColumns();
|
||||
} else if (columnDiff < 0) {
|
||||
const int lastColumn = m_columnCount - 1;
|
||||
q->beginRemoveColumns(QModelIndex(), lastColumn + columnDiff + 1, lastColumn);
|
||||
m_columnCount = newColumnCount;
|
||||
q->endRemoveColumns();
|
||||
}
|
||||
}
|
||||
|
||||
int QConcatenateTablesProxyModelPrivate::columnCountAfterChange(const QAbstractItemModel *model, int newCount) const
|
||||
{
|
||||
int newColumnCount = 0;
|
||||
for (int i = 0; i < m_models.count(); ++i) {
|
||||
const QAbstractItemModel *mod = m_models.at(i);
|
||||
const int colCount = mod == model ? newCount : mod->columnCount();
|
||||
if (i == 0)
|
||||
newColumnCount = colCount;
|
||||
else
|
||||
newColumnCount = qMin(colCount, newColumnCount);
|
||||
}
|
||||
return newColumnCount;
|
||||
}
|
||||
|
||||
int QConcatenateTablesProxyModelPrivate::computeRowsPrior(const QAbstractItemModel *sourceModel) const
|
||||
{
|
||||
int rowsPrior = 0;
|
||||
for (const QAbstractItemModel *model : m_models) {
|
||||
if (model == sourceModel)
|
||||
break;
|
||||
rowsPrior += model->rowCount();
|
||||
}
|
||||
return rowsPrior;
|
||||
}
|
||||
|
||||
QConcatenateTablesProxyModelPrivate::SourceModelForRowResult QConcatenateTablesProxyModelPrivate::sourceModelForRow(int row) const
|
||||
{
|
||||
QConcatenateTablesProxyModelPrivate::SourceModelForRowResult result;
|
||||
int rowCount = 0;
|
||||
for (QAbstractItemModel *model : m_models) {
|
||||
const int subRowCount = model->rowCount();
|
||||
if (rowCount + subRowCount > row) {
|
||||
result.sourceModel = model;
|
||||
break;
|
||||
}
|
||||
rowCount += subRowCount;
|
||||
}
|
||||
result.sourceRow = row - rowCount;
|
||||
return result;
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include "moc_qconcatenatetablesproxymodel.cpp"
|
100
src/corelib/itemmodels/qconcatenatetablesproxymodel.h
Normal file
100
src/corelib/itemmodels/qconcatenatetablesproxymodel.h
Normal file
@ -0,0 +1,100 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtCore module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#ifndef QCONCATENATEROWSPROXYMODEL_H
|
||||
#define QCONCATENATEROWSPROXYMODEL_H
|
||||
|
||||
#include <QtCore/qabstractitemmodel.h>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
class QConcatenateTablesProxyModelPrivate;
|
||||
|
||||
class Q_CORE_EXPORT QConcatenateTablesProxyModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QConcatenateTablesProxyModel(QObject *parent = nullptr);
|
||||
~QConcatenateTablesProxyModel();
|
||||
|
||||
Q_SCRIPTABLE void addSourceModel(QAbstractItemModel *sourceModel);
|
||||
Q_SCRIPTABLE void removeSourceModel(QAbstractItemModel *sourceModel);
|
||||
|
||||
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const;
|
||||
QModelIndex mapToSource(const QModelIndex &proxyIndex) const;
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
QMap<int, QVariant> itemData(const QModelIndex &proxyIndex) const override;
|
||||
bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &roles) override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QStringList mimeTypes() const override;
|
||||
QMimeData *mimeData(const QModelIndexList &indexes) const override;
|
||||
bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override;
|
||||
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
|
||||
QSize span(const QModelIndex &index) const override;
|
||||
|
||||
private:
|
||||
Q_DECLARE_PRIVATE(QConcatenateTablesProxyModel)
|
||||
Q_DISABLE_COPY(QConcatenateTablesProxyModel)
|
||||
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotRowsAboutToBeInserted(const QModelIndex &, int start, int end))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotRowsInserted(const QModelIndex &, int start, int end))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotRowsRemoved(const QModelIndex &, int start, int end))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotColumnsInserted(const QModelIndex &parent, int, int))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotColumnsRemoved(const QModelIndex &parent, int, int))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotSourceLayoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotSourceLayoutChanged(const QList<QPersistentModelIndex> &, QAbstractItemModel::LayoutChangeHint))
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotModelAboutToBeReset())
|
||||
Q_PRIVATE_SLOT(d_func(), void _q_slotModelReset())
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // QCONCATENATEROWSPROXYMODEL_H
|
@ -5,6 +5,7 @@ SUBDIRS = qabstractitemmodel \
|
||||
|
||||
qtHaveModule(gui): SUBDIRS += \
|
||||
qabstractproxymodel \
|
||||
qconcatenatetablesproxymodel \
|
||||
qidentityproxymodel \
|
||||
qitemselectionmodel \
|
||||
qsortfilterproxymodel_recursive \
|
||||
|
@ -0,0 +1,5 @@
|
||||
CONFIG += testcase
|
||||
TARGET = tst_qconcatenatetablesproxymodel
|
||||
QT = core gui testlib
|
||||
|
||||
SOURCES = tst_qconcatenatetablesproxymodel.cpp
|
@ -0,0 +1,823 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtCore module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QTest>
|
||||
#include <QStandardItemModel>
|
||||
#include <QIdentityProxyModel>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QMimeData>
|
||||
#include <QStringListModel>
|
||||
#include <QAbstractItemModelTester>
|
||||
|
||||
#include <qconcatenatetablesproxymodel.h>
|
||||
|
||||
Q_DECLARE_METATYPE(QModelIndex)
|
||||
|
||||
// Extracts a full row from a model as a string
|
||||
// Works best if every cell contains only one character
|
||||
static QString extractRowTexts(QAbstractItemModel *model, int row, const QModelIndex &parent = QModelIndex())
|
||||
{
|
||||
QString result;
|
||||
const int colCount = model->columnCount();
|
||||
for (int col = 0; col < colCount; ++col) {
|
||||
const QString txt = model->index(row, col, parent).data().toString();
|
||||
result += txt.isEmpty() ? QStringLiteral(" ") : txt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Extracts a full column from a model as a string
|
||||
// Works best if every cell contains only one character
|
||||
static QString extractColumnTexts(QAbstractItemModel *model, int column, const QModelIndex &parent = QModelIndex())
|
||||
{
|
||||
QString result;
|
||||
const int rowCount = model->rowCount();
|
||||
for (int row = 0; row < rowCount; ++row) {
|
||||
const QString txt = model->index(row, column, parent).data().toString();
|
||||
result += txt.isEmpty() ? QStringLiteral(" ") : txt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static QString rowSpyToText(const QSignalSpy &spy)
|
||||
{
|
||||
if (!spy.isValid())
|
||||
return QStringLiteral("THE SIGNALSPY IS INVALID!");
|
||||
QString str;
|
||||
for (int i = 0; i < spy.count(); ++i) {
|
||||
str += spy.at(i).at(1).toString() + QLatin1Char(',') + spy.at(i).at(2).toString();
|
||||
if (i + 1 < spy.count())
|
||||
str += QLatin1Char(';');
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
class tst_QConcatenateTablesProxyModel : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void init();
|
||||
void shouldAggregateTwoModelsCorrectly();
|
||||
void shouldAggregateThenRemoveTwoEmptyModelsCorrectly();
|
||||
void shouldAggregateTwoEmptyModelsWhichThenGetFilled();
|
||||
void shouldHandleDataChanged();
|
||||
void shouldHandleSetData();
|
||||
void shouldHandleSetItemData();
|
||||
void shouldHandleRowInsertionAndRemoval();
|
||||
void shouldAggregateAnotherModelThenRemoveModels();
|
||||
void shouldUseSmallestColumnCount();
|
||||
void shouldIncreaseColumnCountWhenRemovingFirstModel();
|
||||
void shouldHandleColumnInsertionAndRemoval();
|
||||
void shouldPropagateLayoutChanged();
|
||||
void shouldReactToModelReset();
|
||||
void shouldUpdateColumnsOnModelReset();
|
||||
void shouldPropagateDropOnItem_data();
|
||||
void shouldPropagateDropOnItem();
|
||||
void shouldPropagateDropBetweenItems();
|
||||
void shouldPropagateDropBetweenItemsAtModelBoundary();
|
||||
void shouldPropagateDropAfterLastRow_data();
|
||||
void shouldPropagateDropAfterLastRow();
|
||||
|
||||
private:
|
||||
QStandardItemModel mod;
|
||||
QStandardItemModel mod2;
|
||||
QStandardItemModel mod3;
|
||||
};
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::init()
|
||||
{
|
||||
// Prepare some source models to use later on
|
||||
mod.clear();
|
||||
mod.appendRow({ new QStandardItem(QStringLiteral("A")), new QStandardItem(QStringLiteral("B")), new QStandardItem(QStringLiteral("C")) });
|
||||
mod.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
|
||||
mod.setVerticalHeaderLabels(QStringList() << QStringLiteral("One"));
|
||||
|
||||
mod2.clear();
|
||||
mod2.appendRow({ new QStandardItem(QStringLiteral("D")), new QStandardItem(QStringLiteral("E")), new QStandardItem(QStringLiteral("F")) });
|
||||
mod2.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
|
||||
mod2.setVerticalHeaderLabels(QStringList() << QStringLiteral("Two"));
|
||||
|
||||
mod3.clear();
|
||||
mod3.appendRow({ new QStandardItem(QStringLiteral("1")), new QStandardItem(QStringLiteral("2")), new QStandardItem(QStringLiteral("3")) });
|
||||
mod3.appendRow({ new QStandardItem(QStringLiteral("4")), new QStandardItem(QStringLiteral("5")), new QStandardItem(QStringLiteral("6")) });
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldAggregateTwoModelsCorrectly()
|
||||
{
|
||||
// Given a combining proxy
|
||||
QConcatenateTablesProxyModel pm;
|
||||
|
||||
// When adding two source models
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
// Then the proxy should show 2 rows
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
|
||||
// ... and correct headers
|
||||
QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
|
||||
QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
|
||||
QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
|
||||
QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
|
||||
QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
|
||||
|
||||
QVERIFY(!pm.canFetchMore(QModelIndex()));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldAggregateThenRemoveTwoEmptyModelsCorrectly()
|
||||
{
|
||||
// Given a combining proxy
|
||||
QConcatenateTablesProxyModel pm;
|
||||
|
||||
// When adding two empty models
|
||||
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QIdentityProxyModel i1, i2;
|
||||
pm.addSourceModel(&i1);
|
||||
pm.addSourceModel(&i2);
|
||||
|
||||
// Then the proxy should still be empty (and no signals emitted)
|
||||
QCOMPARE(pm.rowCount(), 0);
|
||||
QCOMPARE(pm.columnCount(), 0);
|
||||
QCOMPARE(rowATBISpy.count(), 0);
|
||||
QCOMPARE(rowInsertedSpy.count(), 0);
|
||||
|
||||
// When removing the empty models
|
||||
pm.removeSourceModel(&i1);
|
||||
pm.removeSourceModel(&i2);
|
||||
|
||||
// Then the proxy should still be empty (and no signals emitted)
|
||||
QCOMPARE(pm.rowCount(), 0);
|
||||
QCOMPARE(pm.columnCount(), 0);
|
||||
QCOMPARE(rowATBRSpy.count(), 0);
|
||||
QCOMPARE(rowRemovedSpy.count(), 0);
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldAggregateTwoEmptyModelsWhichThenGetFilled()
|
||||
{
|
||||
// Given a combining proxy with two empty models
|
||||
QConcatenateTablesProxyModel pm;
|
||||
QIdentityProxyModel i1, i2;
|
||||
pm.addSourceModel(&i1);
|
||||
pm.addSourceModel(&i2);
|
||||
|
||||
// When filling them afterwards
|
||||
i1.setSourceModel(&mod);
|
||||
i2.setSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
// Then the proxy should show 2 rows
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(pm.columnCount(), 3);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
|
||||
// ... and correct headers
|
||||
QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
|
||||
QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
|
||||
QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
|
||||
QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
|
||||
QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
|
||||
|
||||
QVERIFY(!pm.canFetchMore(QModelIndex()));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldHandleDataChanged()
|
||||
{
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
|
||||
|
||||
// When a cell in a source model changes
|
||||
mod.item(0, 0)->setData("a", Qt::EditRole);
|
||||
|
||||
// Then the change should be notified to the proxy
|
||||
QCOMPARE(dataChangedSpy.count(), 1);
|
||||
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
|
||||
|
||||
// Same test with the other model
|
||||
mod2.item(0, 2)->setData("f", Qt::EditRole);
|
||||
|
||||
QCOMPARE(dataChangedSpy.count(), 2);
|
||||
QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldHandleSetData()
|
||||
{
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
|
||||
|
||||
// When changing a cell using setData
|
||||
pm.setData(pm.index(0, 0), "a");
|
||||
|
||||
// Then the change should be notified to the proxy
|
||||
QCOMPARE(dataChangedSpy.count(), 1);
|
||||
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
|
||||
|
||||
// Same test with the other model
|
||||
pm.setData(pm.index(1, 2), "f");
|
||||
|
||||
QCOMPARE(dataChangedSpy.count(), 2);
|
||||
QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldHandleSetItemData()
|
||||
{
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
|
||||
|
||||
// When changing a cell using setData
|
||||
pm.setItemData(pm.index(0, 0), QMap<int, QVariant>{ std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("X")),
|
||||
std::make_pair<int, QVariant>(Qt::UserRole, 88) });
|
||||
|
||||
// Then the change should be notified to the proxy
|
||||
QCOMPARE(dataChangedSpy.count(), 1);
|
||||
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("XBC"));
|
||||
QCOMPARE(pm.index(0, 0).data(Qt::UserRole).toInt(), 88);
|
||||
|
||||
// Same test with the other model
|
||||
pm.setItemData(pm.index(1, 2), QMap<int, QVariant>{ std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("Y")),
|
||||
std::make_pair<int, QVariant>(Qt::UserRole, 89) });
|
||||
|
||||
QCOMPARE(dataChangedSpy.count(), 2);
|
||||
QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEY"));
|
||||
QCOMPARE(pm.index(1, 2).data(Qt::UserRole).toInt(), 89);
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldHandleRowInsertionAndRemoval()
|
||||
{
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
|
||||
|
||||
// When a source model inserts a new row
|
||||
QList<QStandardItem *> row;
|
||||
row.append(new QStandardItem(QStringLiteral("1")));
|
||||
row.append(new QStandardItem(QStringLiteral("2")));
|
||||
row.append(new QStandardItem(QStringLiteral("3")));
|
||||
mod2.insertRow(0, row);
|
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("1,1"));
|
||||
QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("1,1"));
|
||||
QCOMPARE(pm.rowCount(), 3);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
|
||||
|
||||
// When removing that row
|
||||
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
mod2.removeRow(0);
|
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowATBRSpy.count(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
|
||||
QCOMPARE(rowRemovedSpy.count(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
|
||||
// When removing the last row from mod2
|
||||
rowATBRSpy.clear();
|
||||
rowRemovedSpy.clear();
|
||||
mod2.removeRow(0);
|
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowATBRSpy.count(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
|
||||
QCOMPARE(rowRemovedSpy.count(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
|
||||
QCOMPARE(pm.rowCount(), 1);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldAggregateAnotherModelThenRemoveModels()
|
||||
{
|
||||
// Given two models combined, and a third model
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
|
||||
|
||||
// When adding the new source model
|
||||
pm.addSourceModel(&mod3);
|
||||
|
||||
// Then the proxy should notify its users about the two rows inserted
|
||||
QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("2,3"));
|
||||
QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("2,3"));
|
||||
QCOMPARE(pm.rowCount(), 4);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
|
||||
|
||||
// When removing that source model again
|
||||
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
pm.removeSourceModel(&mod3);
|
||||
|
||||
// Then the proxy should notify its users about the row removed
|
||||
QCOMPARE(rowATBRSpy.count(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 2);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 3);
|
||||
QCOMPARE(rowRemovedSpy.count(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 2);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 3);
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
|
||||
// When removing model 2
|
||||
rowATBRSpy.clear();
|
||||
rowRemovedSpy.clear();
|
||||
pm.removeSourceModel(&mod2);
|
||||
QCOMPARE(rowATBRSpy.count(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
|
||||
QCOMPARE(rowRemovedSpy.count(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
|
||||
QCOMPARE(pm.rowCount(), 1);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
|
||||
// When removing model 1
|
||||
rowATBRSpy.clear();
|
||||
rowRemovedSpy.clear();
|
||||
pm.removeSourceModel(&mod);
|
||||
QCOMPARE(rowATBRSpy.count(), 1);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 0);
|
||||
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 0);
|
||||
QCOMPARE(rowRemovedSpy.count(), 1);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 0);
|
||||
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 0);
|
||||
QCOMPARE(pm.rowCount(), 0);
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldUseSmallestColumnCount()
|
||||
{
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
pm.addSourceModel(&mod2);
|
||||
mod2.setColumnCount(1);
|
||||
pm.addSourceModel(&mod3);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
QCOMPARE(pm.rowCount(), 4);
|
||||
QCOMPARE(pm.columnCount(), 1);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("1"));
|
||||
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("4"));
|
||||
|
||||
const QModelIndex indexA = pm.mapFromSource(mod.index(0, 0));
|
||||
QVERIFY(indexA.isValid());
|
||||
QCOMPARE(indexA, pm.index(0, 0));
|
||||
|
||||
const QModelIndex indexB = pm.mapFromSource(mod.index(0, 1));
|
||||
QVERIFY(!indexB.isValid());
|
||||
|
||||
const QModelIndex indexD = pm.mapFromSource(mod2.index(0, 0));
|
||||
QVERIFY(indexD.isValid());
|
||||
QCOMPARE(indexD, pm.index(1, 0));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldIncreaseColumnCountWhenRemovingFirstModel()
|
||||
{
|
||||
// Given a model with 2 columns and one with 3 columns
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
mod.setColumnCount(2);
|
||||
pm.addSourceModel(&mod2);
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(pm.columnCount(), 2);
|
||||
|
||||
QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
|
||||
// When removing the first source model
|
||||
pm.removeSourceModel(&mod);
|
||||
|
||||
// Then the proxy should notify its users about the row removed, and the column added
|
||||
QCOMPARE(pm.rowCount(), 1);
|
||||
QCOMPARE(pm.columnCount(), 3);
|
||||
QCOMPARE(rowSpyToText(rowATBRSpy), QStringLiteral("0,0"));
|
||||
QCOMPARE(rowSpyToText(rowRemovedSpy), QStringLiteral("0,0"));
|
||||
QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
|
||||
QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("DEF"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldHandleColumnInsertionAndRemoval()
|
||||
{
|
||||
// Given two models combined, one with 2 columns and one with 3
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
mod.setColumnCount(2);
|
||||
pm.addSourceModel(&mod2);
|
||||
QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
|
||||
QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
|
||||
|
||||
// When the first source model inserts a new column
|
||||
QCOMPARE(mod.columnCount(), 2);
|
||||
mod.setColumnCount(3);
|
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
|
||||
QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(pm.columnCount(), 3);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("AB "));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
|
||||
// And when removing two columns
|
||||
mod.setColumnCount(1);
|
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowSpyToText(colATBRSpy), QStringLiteral("1,2"));
|
||||
QCOMPARE(rowSpyToText(colRemovedSpy), QStringLiteral("1,2"));
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(pm.columnCount(), 1);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateLayoutChanged()
|
||||
{
|
||||
// Given two source models, the second one being a QSFPM
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
QSortFilterProxyModel qsfpm;
|
||||
qsfpm.setSourceModel(&mod3);
|
||||
pm.addSourceModel(&qsfpm);
|
||||
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
|
||||
|
||||
// And a selection (row 1)
|
||||
QItemSelectionModel selection(&pm);
|
||||
selection.select(pm.index(1, 0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
|
||||
const QModelIndexList lst = selection.selectedIndexes();
|
||||
QCOMPARE(lst.count(), 3);
|
||||
for (int col = 0; col < lst.count(); ++col) {
|
||||
QCOMPARE(lst.at(col).row(), 1);
|
||||
QCOMPARE(lst.at(col).column(), col);
|
||||
}
|
||||
|
||||
QSignalSpy layoutATBCSpy(&pm, SIGNAL(layoutAboutToBeChanged()));
|
||||
QSignalSpy layoutChangedSpy(&pm, SIGNAL(layoutChanged()));
|
||||
|
||||
// When changing the sorting in the QSFPM
|
||||
qsfpm.sort(0, Qt::DescendingOrder);
|
||||
|
||||
// Then the proxy should emit the layoutChanged signals, and show re-sorted data
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
|
||||
QCOMPARE(layoutATBCSpy.count(), 1);
|
||||
QCOMPARE(layoutChangedSpy.count(), 1);
|
||||
|
||||
// And the selection should be updated accordingly (it became row 2)
|
||||
const QModelIndexList lstAfter = selection.selectedIndexes();
|
||||
QCOMPARE(lstAfter.count(), 3);
|
||||
for (int col = 0; col < lstAfter.count(); ++col) {
|
||||
QCOMPARE(lstAfter.at(col).row(), 2);
|
||||
QCOMPARE(lstAfter.at(col).column(), col);
|
||||
}
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldReactToModelReset()
|
||||
{
|
||||
// Given two source models, the second one being a QSFPM
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
QSortFilterProxyModel qsfpm;
|
||||
qsfpm.setSourceModel(&mod3);
|
||||
pm.addSourceModel(&qsfpm);
|
||||
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
|
||||
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
|
||||
QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
|
||||
QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
|
||||
|
||||
// When changing the source model of the QSFPM
|
||||
qsfpm.setSourceModel(&mod2);
|
||||
|
||||
// Then the proxy should emit the reset signals, and show the new data
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
QCOMPARE(rowATBRSpy.count(), 0);
|
||||
QCOMPARE(rowRemovedSpy.count(), 0);
|
||||
QCOMPARE(rowATBISpy.count(), 0);
|
||||
QCOMPARE(rowInsertedSpy.count(), 0);
|
||||
QCOMPARE(colATBRSpy.count(), 0);
|
||||
QCOMPARE(colRemovedSpy.count(), 0);
|
||||
QCOMPARE(modelATBResetSpy.count(), 1);
|
||||
QCOMPARE(modelResetSpy.count(), 1);
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldUpdateColumnsOnModelReset()
|
||||
{
|
||||
// Given two source models, the first one being a QSFPM
|
||||
QConcatenateTablesProxyModel pm;
|
||||
|
||||
QSortFilterProxyModel qsfpm;
|
||||
qsfpm.setSourceModel(&mod3);
|
||||
pm.addSourceModel(&qsfpm);
|
||||
pm.addSourceModel(&mod);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("ABC"));
|
||||
|
||||
// ... and a model with only 2 columns
|
||||
QStandardItemModel mod2Columns;
|
||||
mod2Columns.appendRow({ new QStandardItem(QStringLiteral("W")), new QStandardItem(QStringLiteral("X")) });
|
||||
|
||||
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
|
||||
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
|
||||
QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
|
||||
QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
|
||||
QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
|
||||
|
||||
// When changing the source model of the QSFPM
|
||||
qsfpm.setSourceModel(&mod2Columns);
|
||||
|
||||
// Then the proxy should reset, and show the new data
|
||||
QCOMPARE(modelATBResetSpy.count(), 1);
|
||||
QCOMPARE(modelResetSpy.count(), 1);
|
||||
QCOMPARE(rowATBRSpy.count(), 0);
|
||||
QCOMPARE(rowRemovedSpy.count(), 0);
|
||||
QCOMPARE(rowATBISpy.count(), 0);
|
||||
QCOMPARE(rowInsertedSpy.count(), 0);
|
||||
QCOMPARE(colATBRSpy.count(), 0);
|
||||
QCOMPARE(colRemovedSpy.count(), 0);
|
||||
|
||||
QCOMPARE(pm.rowCount(), 2);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("WX"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("AB"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem_data()
|
||||
{
|
||||
QTest::addColumn<int>("sourceRow");
|
||||
QTest::addColumn<int>("destRow");
|
||||
QTest::addColumn<QString>("expectedResult");
|
||||
|
||||
QTest::newRow("0-3") << 0 << 3 << QStringLiteral("ABCA");
|
||||
QTest::newRow("1-2") << 1 << 2 << QStringLiteral("ABBD");
|
||||
QTest::newRow("2-1") << 2 << 1 << QStringLiteral("ACCD");
|
||||
QTest::newRow("3-0") << 3 << 0 << QStringLiteral("DBCD");
|
||||
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem()
|
||||
{
|
||||
// Given two source models who handle drops
|
||||
|
||||
// Note: QStandardItemModel handles drop onto items by inserting child rows,
|
||||
// which is good for QTreeView but not for QTableView or QConcatenateTablesProxyModel.
|
||||
// So we use QStringListModel here instead.
|
||||
QConcatenateTablesProxyModel pm;
|
||||
QStringListModel model1({QStringLiteral("A"), QStringLiteral("B")});
|
||||
QStringListModel model2({QStringLiteral("C"), QStringLiteral("D")});
|
||||
pm.addSourceModel(&model1);
|
||||
pm.addSourceModel(&model2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QCOMPARE(extractColumnTexts(&pm, 0), QStringLiteral("ABCD"));
|
||||
|
||||
// When dragging one item
|
||||
QFETCH(int, sourceRow);
|
||||
QMimeData* mimeData = pm.mimeData({pm.index(sourceRow, 0)});
|
||||
QVERIFY(mimeData);
|
||||
|
||||
// and dropping onto another item
|
||||
QFETCH(int, destRow);
|
||||
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
|
||||
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
|
||||
delete mimeData;
|
||||
|
||||
// Then the result should be as expected
|
||||
QFETCH(QString, expectedResult);
|
||||
QCOMPARE(extractColumnTexts(&pm, 0), expectedResult);
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItems()
|
||||
{
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod3);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QCOMPARE(pm.rowCount(), 3);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
|
||||
|
||||
// When dragging the last row
|
||||
QModelIndexList indexes;
|
||||
indexes.reserve(pm.columnCount());
|
||||
for (int col = 0; col < pm.columnCount(); ++col) {
|
||||
indexes.append(pm.index(2, col));
|
||||
}
|
||||
QMimeData* mimeData = pm.mimeData(indexes);
|
||||
QVERIFY(mimeData);
|
||||
|
||||
// and dropping it before row 1
|
||||
const int destRow = 1;
|
||||
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
|
||||
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
|
||||
delete mimeData;
|
||||
|
||||
// Then a new row should be inserted
|
||||
QCOMPARE(pm.rowCount(), 4);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItemsAtModelBoundary()
|
||||
{
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod3);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QCOMPARE(pm.rowCount(), 3);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
|
||||
|
||||
// When dragging the first row
|
||||
QModelIndexList indexes;
|
||||
indexes.reserve(pm.columnCount());
|
||||
for (int col = 0; col < pm.columnCount(); ++col) {
|
||||
indexes.append(pm.index(0, col));
|
||||
}
|
||||
QMimeData* mimeData = pm.mimeData(indexes);
|
||||
QVERIFY(mimeData);
|
||||
|
||||
// and dropping it before row 2
|
||||
const int destRow = 2;
|
||||
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
|
||||
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
|
||||
delete mimeData;
|
||||
|
||||
// Then a new row should be inserted
|
||||
QCOMPARE(pm.rowCount(), 4);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
|
||||
|
||||
// and it should be part of the second model
|
||||
QCOMPARE(mod2.rowCount(), 2);
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow_data()
|
||||
{
|
||||
QTest::addColumn<int>("destRow");
|
||||
|
||||
// Dropping after the last row is documented to be done with destRow == -1.
|
||||
QTest::newRow("-1") << -1;
|
||||
// However, sometimes QTreeView calls dropMimeData with destRow == rowCount...
|
||||
// Not sure if that's a bug or not, but let's support it in the model, just in case.
|
||||
QTest::newRow("3") << 3;
|
||||
}
|
||||
|
||||
void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow()
|
||||
{
|
||||
QFETCH(int, destRow);
|
||||
|
||||
// Given two models combined
|
||||
QConcatenateTablesProxyModel pm;
|
||||
pm.addSourceModel(&mod3);
|
||||
pm.addSourceModel(&mod2);
|
||||
QAbstractItemModelTester modelTest(&pm, this);
|
||||
QCOMPARE(pm.rowCount(), 3);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
|
||||
|
||||
// When dragging the second row
|
||||
QModelIndexList indexes;
|
||||
indexes.reserve(pm.columnCount());
|
||||
for (int col = 0; col < pm.columnCount(); ++col) {
|
||||
indexes.append(pm.index(1, col));
|
||||
}
|
||||
QMimeData* mimeData = pm.mimeData(indexes);
|
||||
QVERIFY(mimeData);
|
||||
|
||||
// and dropping it after the last row
|
||||
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
|
||||
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
|
||||
delete mimeData;
|
||||
|
||||
// Then a new row should be inserted at the end
|
||||
QCOMPARE(pm.rowCount(), 4);
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
|
||||
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
|
||||
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
|
||||
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
|
||||
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_QConcatenateTablesProxyModel)
|
||||
|
||||
#include "tst_qconcatenatetablesproxymodel.moc"
|
@ -1,2 +1,8 @@
|
||||
TEMPLATE = subdirs
|
||||
SUBDIRS = delegate qheaderview qtreeview qtreewidget tableview-span-navigation
|
||||
SUBDIRS = delegate \
|
||||
qconcatenatetablesproxymodel \
|
||||
qheaderview \
|
||||
qtreeview \
|
||||
qtreewidget \
|
||||
tableview-span-navigation \
|
||||
|
||||
|
@ -0,0 +1,88 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtGui module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include <QApplication>
|
||||
#include <QConcatenateTablesProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTableView>
|
||||
#include <QTreeView>
|
||||
|
||||
static void prepareModel(const QString &prefix, QStandardItemModel *model)
|
||||
{
|
||||
for (int row = 0; row < model->rowCount(); ++row) {
|
||||
for (int column = 0; column < model->columnCount(); ++column) {
|
||||
QStandardItem *item = new QStandardItem(prefix + QString(" %1,%2").arg(row).arg(column));
|
||||
item->setDragEnabled(true);
|
||||
item->setDropEnabled(true);
|
||||
model->setItem(row, column, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QStandardItemModel firstModel(4, 4);
|
||||
prepareModel("First", &firstModel);
|
||||
QStandardItemModel secondModel(2, 2);
|
||||
|
||||
QConcatenateTablesProxyModel proxy;
|
||||
proxy.addSourceModel(&firstModel);
|
||||
proxy.addSourceModel(&secondModel);
|
||||
|
||||
prepareModel("Second", &secondModel);
|
||||
|
||||
QTableView tableView;
|
||||
tableView.setWindowTitle("concat proxy, in QTableView");
|
||||
tableView.setDragDropMode(QAbstractItemView::DragDrop);
|
||||
tableView.setModel(&proxy);
|
||||
tableView.show();
|
||||
|
||||
QTreeView treeView;
|
||||
treeView.setWindowTitle("concat proxy, in QTreeView");
|
||||
treeView.setDragDropMode(QAbstractItemView::DragDrop);
|
||||
treeView.setModel(&proxy);
|
||||
treeView.show();
|
||||
|
||||
// For comparison, views on top on QStandardItemModel
|
||||
|
||||
QTableView tableViewTest;
|
||||
tableViewTest.setWindowTitle("first model, in QTableView");
|
||||
tableViewTest.setDragDropMode(QAbstractItemView::DragDrop);
|
||||
tableViewTest.setModel(&firstModel);
|
||||
tableViewTest.show();
|
||||
|
||||
QTreeView treeViewTest;
|
||||
treeViewTest.setWindowTitle("first model, in QTreeView");
|
||||
treeViewTest.setDragDropMode(QAbstractItemView::DragDrop);
|
||||
treeViewTest.setModel(&firstModel);
|
||||
treeViewTest.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
|
||||
TEMPLATE = app
|
||||
TARGET = qconcatenatetablesproxymodel
|
||||
INCLUDEPATH += .
|
||||
|
||||
QT += widgets
|
||||
|
||||
SOURCES += main.cpp
|
Loading…
Reference in New Issue
Block a user