This proxy takes multiple source models and concatenates their rows. REVIEW: 124644wilder
parent
06a48c672a
commit
fe0b5826b1
6 changed files with 912 additions and 0 deletions
@ -0,0 +1,405 @@ |
||||
#include <QSignalSpy> |
||||
#include <QSortFilterProxyModel> |
||||
#include <QTest> |
||||
#include <QStandardItemModel> |
||||
#include <QIdentityProxyModel> |
||||
|
||||
#include <kconcatenaterowsproxymodel.h> |
||||
#include "test_model_helpers.h" |
||||
using namespace TestModelHelpers; |
||||
|
||||
Q_DECLARE_METATYPE(QModelIndex) |
||||
|
||||
class tst_KConcatenateRowsProxyModel : public QObject |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
private Q_SLOTS: |
||||
|
||||
void initTestCase() |
||||
{ |
||||
} |
||||
|
||||
void init() |
||||
{ |
||||
// Prepare some source models to use later on
|
||||
mod.clear(); |
||||
mod.insertRow(0, makeStandardItems(QStringList() << "A" << "B" << "C")); |
||||
mod.setHorizontalHeaderLabels(QStringList() << "H1" << "H2" << "H3"); |
||||
mod.setVerticalHeaderLabels(QStringList() << "One"); |
||||
|
||||
mod2.clear(); |
||||
mod2.insertRow(0, makeStandardItems(QStringList() << "D" << "E" << "F")); |
||||
mod2.setHorizontalHeaderLabels(QStringList() << "H1" << "H2" << "H3"); |
||||
mod2.setVerticalHeaderLabels(QStringList() << "Two"); |
||||
} |
||||
|
||||
void shouldAggregateTwoModelsCorrectly() |
||||
{ |
||||
// Given a combining proxy
|
||||
KConcatenateRowsProxyModel pm; |
||||
|
||||
// When adding two source models
|
||||
pm.addSourceModel(&mod); |
||||
pm.addSourceModel(&mod2); |
||||
|
||||
// Then the proxy should show 2 rows
|
||||
QCOMPARE(pm.rowCount(), 2); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEF")); |
||||
|
||||
// ... and correct headers
|
||||
QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QString("H1")); |
||||
QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QString("H2")); |
||||
QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QString("H3")); |
||||
QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QString("One")); |
||||
QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QString("Two")); |
||||
|
||||
QVERIFY(!pm.canFetchMore(QModelIndex())); |
||||
} |
||||
|
||||
void shouldAggregateThenRemoveTwoEmptyModelsCorrectly() |
||||
{ |
||||
// Given a combining proxy
|
||||
KConcatenateRowsProxyModel 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 shouldAggregateTwoEmptyModelsWhichThenGetFilled() |
||||
{ |
||||
// Given a combining proxy
|
||||
KConcatenateRowsProxyModel pm; |
||||
|
||||
// When adding two empty models
|
||||
QIdentityProxyModel i1, i2; |
||||
pm.addSourceModel(&i1); |
||||
pm.addSourceModel(&i2); |
||||
|
||||
QCOMPARE(pm.rowCount(), 0); |
||||
QCOMPARE(pm.columnCount(), 0); |
||||
|
||||
i1.setSourceModel(&mod); |
||||
i2.setSourceModel(&mod2); |
||||
|
||||
// Then the proxy should show 2 rows
|
||||
QCOMPARE(pm.rowCount(), 2); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEF")); |
||||
|
||||
// ... and correct headers
|
||||
QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QString("H1")); |
||||
QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QString("H2")); |
||||
QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QString("H3")); |
||||
QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QString("One")); |
||||
QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QString("Two")); |
||||
|
||||
QVERIFY(!pm.canFetchMore(QModelIndex())); |
||||
} |
||||
|
||||
void shouldHandleDataChanged() |
||||
{ |
||||
// Given two models combined
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
pm.addSourceModel(&mod2); |
||||
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).value<QModelIndex>(), pm.index(0, 0)); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("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).value<QModelIndex>(), pm.index(1, 2)); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEf")); |
||||
} |
||||
|
||||
void shouldHandleSetData() |
||||
{ |
||||
// Given two models combined
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
pm.addSourceModel(&mod2); |
||||
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex))); |
||||
|
||||
// When changing a cell using setData
|
||||
pm.setData(pm.index(0, 0), "a", Qt::EditRole); |
||||
|
||||
// Then the change should be notified to the proxy
|
||||
QCOMPARE(dataChangedSpy.count(), 1); |
||||
QCOMPARE(dataChangedSpy.at(0).at(0).value<QModelIndex>(), pm.index(0, 0)); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("aBC")); |
||||
|
||||
// Same test with the other model
|
||||
pm.setData(pm.index(1, 2), "f", Qt::EditRole); |
||||
|
||||
QCOMPARE(dataChangedSpy.count(), 2); |
||||
QCOMPARE(dataChangedSpy.at(1).at(0).value<QModelIndex>(), pm.index(1, 2)); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEf")); |
||||
} |
||||
|
||||
void shouldHandleRowInsertionAndRemoval() |
||||
{ |
||||
// Given two models combined
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
pm.addSourceModel(&mod2); |
||||
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("1")); |
||||
row.append(new QStandardItem("2")); |
||||
row.append(new QStandardItem("3")); |
||||
mod2.insertRow(0, row); |
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowSpyToText(rowATBISpy), QString("1,1")); |
||||
QCOMPARE(rowSpyToText(rowInsertedSpy), QString("1,1")); |
||||
QCOMPARE(pm.rowCount(), 3); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("123")); |
||||
QCOMPARE(extractRowTexts(&pm, 2), QString("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), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("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), QString("ABC")); |
||||
} |
||||
|
||||
void shouldAggregateAnotherModelThenRemoveModels() |
||||
{ |
||||
// Given two models combined, and a third model
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
pm.addSourceModel(&mod2); |
||||
|
||||
QStandardItemModel mod3; |
||||
mod3.appendRow(makeStandardItems(QStringList() << "1" << "2" << "3")); |
||||
mod3.appendRow(makeStandardItems(QStringList() << "4" << "5" << "6")); |
||||
|
||||
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), QString("2,3")); |
||||
QCOMPARE(rowSpyToText(rowInsertedSpy), QString("2,3")); |
||||
QCOMPARE(pm.rowCount(), 4); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEF")); |
||||
QCOMPARE(extractRowTexts(&pm, 2), QString("123")); |
||||
QCOMPARE(extractRowTexts(&pm, 3), QString("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), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("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), QString("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 shouldHandleColumnInsertionAndRemoval() |
||||
{ |
||||
// Given two models combined
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
pm.addSourceModel(&mod2); |
||||
QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int))); |
||||
QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int))); |
||||
|
||||
// When the first source model inserts a new column
|
||||
QCOMPARE(mod.columnCount(), 3); |
||||
mod.setColumnCount(4); |
||||
|
||||
// Then the proxy should notify its users and show changes
|
||||
QCOMPARE(rowSpyToText(colATBISpy), QString("3,3")); |
||||
QCOMPARE(rowSpyToText(colInsertedSpy), QString("3,3")); |
||||
QCOMPARE(pm.rowCount(), 2); |
||||
QCOMPARE(pm.columnCount(), 4); |
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC ")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEF ")); |
||||
} |
||||
|
||||
void shouldPropagateLayoutChanged() |
||||
{ |
||||
// Given two source models, the second one being a QSFPM
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
|
||||
QStandardItemModel mod3; |
||||
QList<QStandardItem *> row; |
||||
row.append(new QStandardItem("1")); |
||||
row.append(new QStandardItem("2")); |
||||
row.append(new QStandardItem("3")); |
||||
mod3.insertRow(0, row); |
||||
row.clear(); |
||||
row.append(new QStandardItem("4")); |
||||
row.append(new QStandardItem("5")); |
||||
row.append(new QStandardItem("6")); |
||||
mod3.appendRow(row); |
||||
|
||||
QSortFilterProxyModel qsfpm; |
||||
qsfpm.setSourceModel(&mod3); |
||||
pm.addSourceModel(&qsfpm); |
||||
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("123")); |
||||
QCOMPARE(extractRowTexts(&pm, 2), QString("456")); |
||||
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), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("456")); |
||||
QCOMPARE(extractRowTexts(&pm, 2), QString("123")); |
||||
QCOMPARE(layoutATBCSpy.count(), 1); |
||||
QCOMPARE(layoutChangedSpy.count(), 1); |
||||
} |
||||
|
||||
void shouldReactToModelReset() |
||||
{ |
||||
// Given two source models, the second one being a QSFPM
|
||||
KConcatenateRowsProxyModel pm; |
||||
pm.addSourceModel(&mod); |
||||
|
||||
QStandardItemModel mod3; |
||||
QList<QStandardItem *> row; |
||||
row.append(new QStandardItem("1")); |
||||
row.append(new QStandardItem("2")); |
||||
row.append(new QStandardItem("3")); |
||||
mod3.insertRow(0, row); |
||||
row.clear(); |
||||
row.append(new QStandardItem("4")); |
||||
row.append(new QStandardItem("5")); |
||||
row.append(new QStandardItem("6")); |
||||
mod3.appendRow(row); |
||||
|
||||
QSortFilterProxyModel qsfpm; |
||||
qsfpm.setSourceModel(&mod3); |
||||
pm.addSourceModel(&qsfpm); |
||||
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("123")); |
||||
QCOMPARE(extractRowTexts(&pm, 2), QString("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))); |
||||
|
||||
// When changing the source model of the QSFPM
|
||||
qsfpm.setSourceModel(&mod2); |
||||
|
||||
// Then the proxy should emit the row removed/inserted signals, and show the new data
|
||||
QCOMPARE(extractRowTexts(&pm, 0), QString("ABC")); |
||||
QCOMPARE(extractRowTexts(&pm, 1), QString("DEF")); |
||||
QCOMPARE(rowSpyToText(rowATBRSpy), QString("1,2")); |
||||
QCOMPARE(rowSpyToText(rowRemovedSpy), QString("1,2")); |
||||
QCOMPARE(rowSpyToText(rowATBISpy), QString("1,1")); |
||||
QCOMPARE(rowSpyToText(rowInsertedSpy), QString("1,1")); |
||||
} |
||||
|
||||
private: |
||||
QStandardItemModel mod; |
||||
QStandardItemModel mod2; |
||||
}; |
||||
|
||||
QTEST_MAIN(tst_KConcatenateRowsProxyModel) |
||||
|
||||
#include "kconcatenaterowsproxymodeltest.moc" |
||||
@ -0,0 +1,354 @@ |
||||
/*
|
||||
Copyright (c) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com |
||||
Authors: David Faure <david.faure@kdab.com> |
||||
|
||||
This library is free software; you can redistribute it and/or modify it |
||||
under the terms of the GNU Library General Public License as published by |
||||
the Free Software Foundation; either version 2 of the License, or (at your |
||||
option) any later version. |
||||
|
||||
This library is distributed in the hope that it will be useful, but WITHOUT |
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public |
||||
License for more details. |
||||
|
||||
You should have received a copy of the GNU Library General Public License |
||||
along with this library; see the file COPYING.LIB. If not, write to the |
||||
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
||||
02110-1301, USA. |
||||
*/ |
||||
|
||||
#include "kconcatenaterowsproxymodel.h" |
||||
|
||||
class KConcatenateRowsProxyModelPrivate |
||||
{ |
||||
public: |
||||
KConcatenateRowsProxyModelPrivate(KConcatenateRowsProxyModel* model) |
||||
: q(model), |
||||
m_rowCount(0) |
||||
{} |
||||
|
||||
int computeRowsPrior(const QAbstractItemModel *sourceModel) const; |
||||
QAbstractItemModel *sourceModelForRow(int row, int *sourceRow) const; |
||||
|
||||
void slotRowsAboutToBeInserted(const QModelIndex &, int start, int end); |
||||
void slotRowsInserted(const QModelIndex &, int start, int end); |
||||
void slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end); |
||||
void slotRowsRemoved(const QModelIndex &, int start, int end); |
||||
void slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end); |
||||
void slotColumnsInserted(const QModelIndex &parent, int, int); |
||||
void slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end); |
||||
void slotColumnsRemoved(const QModelIndex &parent, int, int); |
||||
void slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles); |
||||
void slotModelAboutToBeReset(); |
||||
void slotModelReset(); |
||||
|
||||
KConcatenateRowsProxyModel *q; |
||||
QList<QAbstractItemModel *> m_models; |
||||
int m_rowCount; // have to maintain it here since we can't compute during model destruction
|
||||
}; |
||||
|
||||
KConcatenateRowsProxyModel::KConcatenateRowsProxyModel(QObject *parent) |
||||
: QAbstractItemModel(parent), |
||||
d(new KConcatenateRowsProxyModelPrivate(this)) |
||||
{ |
||||
} |
||||
|
||||
KConcatenateRowsProxyModel::~KConcatenateRowsProxyModel() |
||||
{ |
||||
} |
||||
|
||||
QModelIndex KConcatenateRowsProxyModel::mapFromSource(const QModelIndex &sourceIndex) const |
||||
{ |
||||
const QAbstractItemModel *sourceModel = sourceIndex.model(); |
||||
int rowsPrior = d->computeRowsPrior(sourceModel); |
||||
return createIndex(rowsPrior + sourceIndex.row(), sourceIndex.column()); |
||||
} |
||||
|
||||
QModelIndex KConcatenateRowsProxyModel::mapToSource(const QModelIndex &proxyIndex) const |
||||
{ |
||||
if (!proxyIndex.isValid()) { |
||||
return QModelIndex(); |
||||
} |
||||
const int row = proxyIndex.row(); |
||||
int sourceRow; |
||||
QAbstractItemModel *sourceModel = d->sourceModelForRow(row, &sourceRow); |
||||
if (!sourceModel) { |
||||
return QModelIndex(); |
||||
} |
||||
return sourceModel->index(sourceRow, proxyIndex.column()); |
||||
} |
||||
|
||||
QVariant KConcatenateRowsProxyModel::data(const QModelIndex &index, int role) const |
||||
{ |
||||
const QModelIndex sourceIndex = mapToSource(index); |
||||
if (!sourceIndex.isValid()) { |
||||
return QVariant(); |
||||
} |
||||
return sourceIndex.model()->data(sourceIndex, role); |
||||
} |
||||
|
||||
bool KConcatenateRowsProxyModel::setData(const QModelIndex &index, const QVariant &value, int role) |
||||
{ |
||||
const QModelIndex sourceIndex = mapToSource(index); |
||||
if (!sourceIndex.isValid()) { |
||||
return false; |
||||
} |
||||
QAbstractItemModel *sourceModel = const_cast<QAbstractItemModel *>(sourceIndex.model()); |
||||
return sourceModel->setData(sourceIndex, value, role); |
||||
} |
||||
|
||||
QMap<int, QVariant> KConcatenateRowsProxyModel::itemData(const QModelIndex &proxyIndex) const |
||||
{ |
||||
const QModelIndex sourceIndex = mapToSource(proxyIndex); |
||||
return sourceIndex.model()->itemData(sourceIndex); |
||||
} |
||||
|
||||
Qt::ItemFlags KConcatenateRowsProxyModel::flags(const QModelIndex &index) const |
||||
{ |
||||
const QModelIndex sourceIndex = mapToSource(index); |
||||
return sourceIndex.model()->flags(sourceIndex); |
||||
} |
||||
|
||||
QVariant KConcatenateRowsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const |
||||
{ |
||||
if (d->m_models.isEmpty()) { |
||||
return QVariant(); |
||||
} |
||||
if (orientation == Qt::Horizontal) { |
||||
return d->m_models.at(0)->headerData(section, orientation, role); |
||||
} else { |
||||
int sourceRow; |
||||
QAbstractItemModel *sourceModel = d->sourceModelForRow(section, &sourceRow); |
||||
if (!sourceModel) { |
||||
return QVariant(); |
||||
} |
||||
return sourceModel->headerData(sourceRow, orientation, role); |
||||
} |
||||
} |
||||
|
||||
int KConcatenateRowsProxyModel::columnCount(const QModelIndex &parent) const |
||||
{ |
||||
if (d->m_models.isEmpty()) { |
||||
return 0; |
||||
} |
||||
if (parent.isValid()) { |
||||
return 0; // flat model;
|
||||
} |
||||
return d->m_models.at(0)->columnCount(QModelIndex()); |
||||
} |
||||
|
||||
QModelIndex KConcatenateRowsProxyModel::index(int row, int column, const QModelIndex &parent) const |
||||
{ |
||||
Q_ASSERT(row >= 0); |
||||
Q_ASSERT(column >= 0); |
||||
int sourceRow; |
||||
QAbstractItemModel *sourceModel = d->sourceModelForRow(row, &sourceRow); |
||||
if (!sourceModel) { |
||||
return QModelIndex(); |
||||
} |
||||
return mapFromSource(sourceModel->index(sourceRow, column, parent)); |
||||
} |
||||
|
||||
QModelIndex KConcatenateRowsProxyModel::parent(const QModelIndex &) const |
||||
{ |
||||
return QModelIndex(); // we are flat, no hierarchy
|
||||
} |
||||
|
||||
int KConcatenateRowsProxyModel::rowCount(const QModelIndex &parent) const |
||||
{ |
||||
if (parent.isValid()) { |
||||
return 0; // flat model
|
||||
} |
||||
|
||||
return d->m_rowCount; |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModel::addSourceModel(QAbstractItemModel *sourceModel) |
||||
{ |
||||
Q_ASSERT(sourceModel); |
||||
Q_ASSERT(!d->m_models.contains(sourceModel)); |
||||
connect(sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)), this, SLOT(slotDataChanged(QModelIndex,QModelIndex,QVector<int>))); |
||||
connect(sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(slotRowsInserted(QModelIndex,int,int))); |
||||
connect(sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(slotRowsRemoved(QModelIndex,int,int))); |
||||
connect(sourceModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(slotRowsAboutToBeInserted(QModelIndex,int,int))); |
||||
connect(sourceModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(slotRowsAboutToBeRemoved(QModelIndex,int,int))); |
||||
|
||||
connect(sourceModel, SIGNAL(columnsInserted(QModelIndex,int,int)), this, SLOT(slotColumnsInserted(QModelIndex,int,int))); |
||||
connect(sourceModel, SIGNAL(columnsRemoved(QModelIndex,int,int)), this, SLOT(slotColumnsRemoved(QModelIndex,int,int))); |
||||
connect(sourceModel, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), this, SLOT(slotColumnsAboutToBeInserted(QModelIndex,int,int))); |
||||
connect(sourceModel, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), this, SLOT(slotColumnsAboutToBeRemoved(QModelIndex,int,int))); |
||||
|
||||
connect(sourceModel, SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)), |
||||
this, SIGNAL(layoutAboutToBeChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint))); |
||||
connect(sourceModel, SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint)), |
||||
this, SIGNAL(layoutChanged(QList<QPersistentModelIndex>, QAbstractItemModel::LayoutChangeHint))); |
||||
connect(sourceModel, SIGNAL(modelAboutToBeReset()), this, SLOT(slotModelAboutToBeReset())); |
||||
connect(sourceModel, SIGNAL(modelReset()), this, SLOT(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(); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModel::removeSourceModel(QAbstractItemModel *sourceModel) |
||||
{ |
||||
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(); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotRowsAboutToBeInserted(const QModelIndex &, int start, int end) |
||||
{ |
||||
const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender()); |
||||
const int rowsPrior = computeRowsPrior(model); |
||||
q->beginInsertRows(QModelIndex(), rowsPrior + start, rowsPrior + end); |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotRowsInserted(const QModelIndex &, int start, int end) |
||||
{ |
||||
m_rowCount += end - start + 1; |
||||
q->endInsertRows(); |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end) |
||||
{ |
||||
const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender()); |
||||
const int rowsPrior = computeRowsPrior(model); |
||||
q->beginRemoveRows(QModelIndex(), rowsPrior + start, rowsPrior + end); |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotRowsRemoved(const QModelIndex &, int start, int end) |
||||
{ |
||||
m_rowCount -= end - start + 1; |
||||
q->endRemoveRows(); |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end) |
||||
{ |
||||
if (parent.isValid()) { // we are flat
|
||||
return; |
||||
} |
||||
const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender()); |
||||
if (m_models.at(0) == model) { |
||||
q->beginInsertColumns(QModelIndex(), start, end); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotColumnsInserted(const QModelIndex &parent, int, int) |
||||
{ |
||||
if (parent.isValid()) { // we are flat
|
||||
return; |
||||
} |
||||
const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender()); |
||||
if (m_models.at(0) == model) { |
||||
q->endInsertColumns(); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end) |
||||
{ |
||||
if (parent.isValid()) { // we are flat
|
||||
return; |
||||
} |
||||
const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender()); |
||||
if (m_models.at(0) == model) { |
||||
q->beginRemoveColumns(QModelIndex(), start, end); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotColumnsRemoved(const QModelIndex &parent, int, int) |
||||
{ |
||||
if (parent.isValid()) { // we are flat
|
||||
return; |
||||
} |
||||
const QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(q->sender()); |
||||
if (m_models.at(0) == model) { |
||||
q->endRemoveColumns(); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles) |
||||
{ |
||||
if (!from.isValid()) { // QSFPM bug, it emits dataChanged(invalid, invalid) if a cell in a hidden column changes
|
||||
return; |
||||
} |
||||
const QModelIndex myFrom = q->mapFromSource(from); |
||||
const QModelIndex myTo = q->mapFromSource(to); |
||||
emit q->dataChanged(myFrom, myTo, roles); |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotModelAboutToBeReset() |
||||
{ |
||||
const QAbstractItemModel *sourceModel = qobject_cast<const QAbstractItemModel *>(q->sender()); |
||||
Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(sourceModel))); |
||||
const int oldRows = sourceModel->rowCount(); |
||||
if (oldRows > 0) { |
||||
slotRowsAboutToBeRemoved(QModelIndex(), 0, oldRows - 1); |
||||
slotRowsRemoved(QModelIndex(), 0, oldRows - 1); |
||||
} |
||||
if (m_models.at(0) == sourceModel) { |
||||
q->beginResetModel(); |
||||
} |
||||
} |
||||
|
||||
void KConcatenateRowsProxyModelPrivate::slotModelReset() |
||||
{ |
||||
const QAbstractItemModel *sourceModel = qobject_cast<const QAbstractItemModel *>(q->sender()); |
||||
Q_ASSERT(m_models.contains(const_cast<QAbstractItemModel *>(sourceModel))); |
||||
if (m_models.at(0) == sourceModel) { |
||||
q->endResetModel(); |
||||
} |
||||
const int newRows = sourceModel->rowCount(); |
||||
if (newRows > 0) { |
||||
slotRowsAboutToBeInserted(QModelIndex(), 0, newRows - 1); |
||||
slotRowsInserted(QModelIndex(), 0, newRows - 1); |
||||
} |
||||
} |
||||
|
||||
int KConcatenateRowsProxyModelPrivate::computeRowsPrior(const QAbstractItemModel *sourceModel) const |
||||
{ |
||||
int rowsPrior = 0; |
||||
foreach (const QAbstractItemModel *model, m_models) { |
||||
if (model == sourceModel) { |
||||
break; |
||||
} |
||||
rowsPrior += model->rowCount(); |
||||
} |
||||
return rowsPrior; |
||||
} |
||||
|
||||
QAbstractItemModel *KConcatenateRowsProxyModelPrivate::sourceModelForRow(int row, int *sourceRow) const |
||||
{ |
||||
int rowCount = 0; |
||||
QAbstractItemModel *selection = NULL; |
||||
foreach (QAbstractItemModel *model, m_models) { |
||||
const int subRowCount = model->rowCount(); |
||||
if (rowCount + subRowCount > row) { |
||||
selection = model; |
||||
break; |
||||
} |
||||
rowCount += subRowCount; |
||||
} |
||||
*sourceRow = row - rowCount; |
||||
return selection; |
||||
} |
||||
|
||||
#include "moc_kconcatenaterowsproxymodel.cpp" |
||||
@ -0,0 +1,149 @@ |
||||
/*
|
||||
Copyright (c) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com |
||||
Authors: David Faure <david.faure@kdab.com> |
||||
|
||||
This library is free software; you can redistribute it and/or modify it |
||||
under the terms of the GNU Library General Public License as published by |
||||
the Free Software Foundation; either version 2 of the License, or (at your |
||||
option) any later version. |
||||
|
||||
This library is distributed in the hope that it will be useful, but WITHOUT |
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public |
||||
License for more details. |
||||
|
||||
You should have received a copy of the GNU Library General Public License |
||||
along with this library; see the file COPYING.LIB. If not, write to the |
||||
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
||||
02110-1301, USA. |
||||
*/ |
||||
|
||||
#ifndef KCONCATENATEROWSPROXYMODEL_H |
||||
#define KCONCATENATEROWSPROXYMODEL_H |
||||
|
||||
#include <QAbstractItemModel> |
||||
#include <QScopedPointer> |
||||
#include "kitemmodels_export.h" |
||||
|
||||
class KConcatenateRowsProxyModelPrivate; |
||||
|
||||
/**
|
||||
* This proxy 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, etc. |
||||
* |
||||
* Only flat models (lists and tables) are supported, no trees. |
||||
* |
||||
* All models are assumed to have the same number of columns. |
||||
* More precisely, the number of columns of the first source model is used, |
||||
* so all source models should have at least as many columns as the first source model, |
||||
* and additional columns in other source models will simply be ignored. |
||||
* |
||||
* Source models can be added and removed at runtime, including the first source |
||||
* model (but it should keep the same number of columns). |
||||
* |
||||
* Dynamic insertion and removal of rows and columns in any source model is supported. |
||||
* dataChanged, layoutChanged and reset coming from the source models are supported. |
||||
* |
||||
* At the moment this model doesn't support editing, drag-n-drop. |
||||
* It could be added though, nothing prevents it. |
||||
* |
||||
* This proxy does not inherit from QAbstractProxyModel because it uses multiple source |
||||
* models, rather than a single one. |
||||
* |
||||
* Author: David Faure, KDAB |
||||
* @since 5.14 |
||||
*/ |
||||
class KITEMMODELS_EXPORT KConcatenateRowsProxyModel : public QAbstractItemModel |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
/**
|
||||
* Creates a KConcatenateRowsProxyModel. |
||||
* @param parent optional parent |
||||
*/ |
||||
KConcatenateRowsProxyModel(QObject *parent = 0); |
||||
/**
|
||||
* Destructor. |
||||
*/ |
||||
virtual ~KConcatenateRowsProxyModel(); |
||||
|
||||
/**
|
||||
* Adds a source model @p sourceModel, after all existing source models. |
||||
* @param sourceModel the source model |
||||
* |
||||
* The ownership of @p sourceModel is not affected by this. |
||||
* The same source model cannot be added more than once. |
||||
*/ |
||||
void addSourceModel(QAbstractItemModel *sourceModel); |
||||
|
||||
/**
|
||||
* Removes the source model @p sourceModel. |
||||
* @param sourceModel a source model previously added to this proxy |
||||
* |
||||
* The ownership of @sourceModel is not affected by this. |
||||
*/ |
||||
void removeSourceModel(QAbstractItemModel *sourceModel); |
||||
|
||||
/**
|
||||
* Returns the proxy index for a given source index |
||||
* @param sourceIndex an index coming from any of the source models |
||||
* @return a proxy index |
||||
* Calling this method with an index not from a source model is undefined behavior. |
||||
*/ |
||||
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; |
||||
|
||||
/**
|
||||
* Returns the source index for a given proxy index. |
||||
* @param proxyIndex an index for this proxy model |
||||
* @return a source index |
||||
*/ |
||||
QModelIndex mapToSource(const QModelIndex &proxyIndex) const; |
||||
|
||||
/// @reimp
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; |
||||
/// @reimp
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::DisplayRole) Q_DECL_OVERRIDE; |
||||
/// @reimp
|
||||
QMap<int, QVariant> itemData(const QModelIndex &proxyIndex) const Q_DECL_OVERRIDE; |
||||
/// @reimp
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; |
||||
/// @reimp
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; |
||||
/// @reimp
|
||||
QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; |
||||
/// @reimp
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; |
||||
|
||||
/**
|
||||
* The horizontal header data for the first source model is returned here. |
||||
* @reimp |
||||
*/ |
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; |
||||
/**
|
||||
* The column count for the first source model is returned here. |
||||
* @reimp |
||||
*/ |
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; |
||||
|
||||
private: |
||||
Q_PRIVATE_SLOT(d, void slotRowsAboutToBeInserted(const QModelIndex &, int start, int end)) |
||||
Q_PRIVATE_SLOT(d, void slotRowsInserted(const QModelIndex &, int start, int end)) |
||||
Q_PRIVATE_SLOT(d, void slotRowsAboutToBeRemoved(const QModelIndex &, int start, int end)) |
||||
Q_PRIVATE_SLOT(d, void slotRowsRemoved(const QModelIndex &, int start, int end)) |
||||
Q_PRIVATE_SLOT(d, void slotColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)) |
||||
Q_PRIVATE_SLOT(d, void slotColumnsInserted(const QModelIndex &parent, int, int)) |
||||
Q_PRIVATE_SLOT(d, void slotColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)) |
||||
Q_PRIVATE_SLOT(d, void slotColumnsRemoved(const QModelIndex &parent, int, int)) |
||||
Q_PRIVATE_SLOT(d, void slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector<int> &roles)) |
||||
Q_PRIVATE_SLOT(d, void slotModelAboutToBeReset()) |
||||
Q_PRIVATE_SLOT(d, void slotModelReset()) |
||||
|
||||
private: |
||||
friend class KConcatenateRowsProxyModelPrivate; |
||||
const QScopedPointer<KConcatenateRowsProxyModelPrivate> d; |
||||
}; |
||||
|
||||
#endif |
||||
Loading…
Reference in new issue