diff --git a/src/corelib/configure.json b/src/corelib/configure.json index 183eb3a13e..f09ef6c1dd 100644 --- a/src/corelib/configure.json +++ b/src/corelib/configure.json @@ -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.", diff --git a/src/corelib/itemmodels/itemmodels.pri b/src/corelib/itemmodels/itemmodels.pri index 068a8c4b3a..5a977c6623 100644 --- a/src/corelib/itemmodels/itemmodels.pri +++ b/src/corelib/itemmodels/itemmodels.pri @@ -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 diff --git a/src/corelib/itemmodels/qconcatenatetablesproxymodel.cpp b/src/corelib/itemmodels/qconcatenatetablesproxymodel.cpp new file mode 100644 index 0000000000..bbfe2dce16 --- /dev/null +++ b/src/corelib/itemmodels/qconcatenatetablesproxymodel.cpp @@ -0,0 +1,750 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure +** 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 +#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 &roles); + void _q_slotSourceLayoutAboutToBeChanged(const QList &sourceParents, QAbstractItemModel::LayoutChangeHint hint); + void _q_slotSourceLayoutChanged(const QList &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 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 layoutChangePersistentIndexes; + QVector 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(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(sourceIndex.model()); + return sourceModel->setData(sourceIndex, value, role); +} + +/*! + \reimp +*/ +QMap 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 &roles) +{ + Q_ASSERT(checkIndex(proxyIndex)); + const QModelIndex sourceIndex = mapToSource(proxyIndex); + Q_ASSERT(sourceIndex.isValid()); + const auto sourceModel = const_cast(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)), this, SLOT(_q_slotDataChanged(QModelIndex,QModelIndex,QVector))); + 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, QAbstractItemModel::LayoutChangeHint)), + this, SLOT(_q_slotSourceLayoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint))); + connect(sourceModel, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, SLOT(_q_slotSourceLayoutChanged(QList, 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(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(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(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(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 &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 &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 &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(static_cast(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(static_cast(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" diff --git a/src/corelib/itemmodels/qconcatenatetablesproxymodel.h b/src/corelib/itemmodels/qconcatenatetablesproxymodel.h new file mode 100644 index 0000000000..85fc6a9c72 --- /dev/null +++ b/src/corelib/itemmodels/qconcatenatetablesproxymodel.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure +** 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 + +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 itemData(const QModelIndex &proxyIndex) const override; + bool setItemData(const QModelIndex &index, const QMap &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 &roles)) + Q_PRIVATE_SLOT(d_func(), void _q_slotSourceLayoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint)) + Q_PRIVATE_SLOT(d_func(), void _q_slotSourceLayoutChanged(const QList &, QAbstractItemModel::LayoutChangeHint)) + Q_PRIVATE_SLOT(d_func(), void _q_slotModelAboutToBeReset()) + Q_PRIVATE_SLOT(d_func(), void _q_slotModelReset()) +}; + +QT_END_NAMESPACE + +#endif // QCONCATENATEROWSPROXYMODEL_H diff --git a/tests/auto/corelib/itemmodels/itemmodels.pro b/tests/auto/corelib/itemmodels/itemmodels.pro index bcb6e604f8..0346341be6 100644 --- a/tests/auto/corelib/itemmodels/itemmodels.pro +++ b/tests/auto/corelib/itemmodels/itemmodels.pro @@ -5,6 +5,7 @@ SUBDIRS = qabstractitemmodel \ qtHaveModule(gui): SUBDIRS += \ qabstractproxymodel \ + qconcatenatetablesproxymodel \ qidentityproxymodel \ qitemselectionmodel \ qsortfilterproxymodel_recursive \ diff --git a/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/qconcatenatetablesproxymodel.pro b/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/qconcatenatetablesproxymodel.pro new file mode 100644 index 0000000000..ee4ea28b5b --- /dev/null +++ b/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/qconcatenatetablesproxymodel.pro @@ -0,0 +1,5 @@ +CONFIG += testcase +TARGET = tst_qconcatenatetablesproxymodel +QT = core gui testlib + +SOURCES = tst_qconcatenatetablesproxymodel.cpp diff --git a/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/tst_qconcatenatetablesproxymodel.cpp b/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/tst_qconcatenatetablesproxymodel.cpp new file mode 100644 index 0000000000..40617c1f7d --- /dev/null +++ b/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/tst_qconcatenatetablesproxymodel.cpp @@ -0,0 +1,823 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure +** 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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{ std::make_pair(Qt::DisplayRole, QStringLiteral("X")), + std::make_pair(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{ std::make_pair(Qt::DisplayRole, QStringLiteral("Y")), + std::make_pair(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 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("sourceRow"); + QTest::addColumn("destRow"); + QTest::addColumn("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("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" diff --git a/tests/manual/widgets/itemviews/itemviews.pro b/tests/manual/widgets/itemviews/itemviews.pro index 53f658d54d..8884cc3aae 100644 --- a/tests/manual/widgets/itemviews/itemviews.pro +++ b/tests/manual/widgets/itemviews/itemviews.pro @@ -1,2 +1,8 @@ TEMPLATE = subdirs -SUBDIRS = delegate qheaderview qtreeview qtreewidget tableview-span-navigation +SUBDIRS = delegate \ + qconcatenatetablesproxymodel \ + qheaderview \ + qtreeview \ + qtreewidget \ + tableview-span-navigation \ + diff --git a/tests/manual/widgets/itemviews/qconcatenatetablesproxymodel/main.cpp b/tests/manual/widgets/itemviews/qconcatenatetablesproxymodel/main.cpp new file mode 100644 index 0000000000..2c1825f29f --- /dev/null +++ b/tests/manual/widgets/itemviews/qconcatenatetablesproxymodel/main.cpp @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure +** 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 +#include +#include +#include +#include + +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(); +} diff --git a/tests/manual/widgets/itemviews/qconcatenatetablesproxymodel/qconcatenatetablesproxymodel.pro b/tests/manual/widgets/itemviews/qconcatenatetablesproxymodel/qconcatenatetablesproxymodel.pro new file mode 100644 index 0000000000..19904212a7 --- /dev/null +++ b/tests/manual/widgets/itemviews/qconcatenatetablesproxymodel/qconcatenatetablesproxymodel.pro @@ -0,0 +1,8 @@ + +TEMPLATE = app +TARGET = qconcatenatetablesproxymodel +INCLUDEPATH += . + +QT += widgets + +SOURCES += main.cpp