diff --git a/README.md b/README.md index 55cbd3a..2ab2a9a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ KItemModels provides the following models: * KBreadcrumbSelectionModel - Selects the parents of selected items to create breadcrumbs * KCheckableProxyModel - Adds a checkable capability to a source model +* KConcatenateRowsProxyModel - Concatenates rows from multiple source models * KDescendantsProxyModel - Proxy Model for restructuring a Tree into a list * KExtraColumnsProxyModel - Adds columns after existing columns * KLinkItemSelectionModel - Share a selection in multiple views which do not diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 0cf8faf..9a838d4 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -15,6 +15,7 @@ add_subdirectory(proxymodeltestsuite) include(ECMAddTests) ecm_add_tests( + kconcatenaterowsproxymodeltest.cpp kdescendantsproxymodel_smoketest.cpp kextracolumnsproxymodeltest.cpp klinkitemselectionmodeltest.cpp diff --git a/autotests/kconcatenaterowsproxymodeltest.cpp b/autotests/kconcatenaterowsproxymodeltest.cpp new file mode 100644 index 0000000..06745de --- /dev/null +++ b/autotests/kconcatenaterowsproxymodeltest.cpp @@ -0,0 +1,405 @@ +#include +#include +#include +#include +#include + +#include +#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(), 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(), 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(), 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(), 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 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 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 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" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fad530d..f8d76db 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ set(kitemmodels_SRCS kbreadcrumbselectionmodel.cpp kcheckableproxymodel.cpp + kconcatenaterowsproxymodel.cpp kdescendantsproxymodel.cpp kextracolumnsproxymodel.cpp klinkitemselectionmodel.cpp @@ -27,6 +28,7 @@ set_target_properties(KF5ItemModels PROPERTIES VERSION ${KITEMMODELS_VERSION_S ecm_generate_headers(KItemModels_HEADERS HEADER_NAMES KBreadcrumbSelectionModel + KConcatenateRowsProxyModel KCheckableProxyModel KExtraColumnsProxyModel KLinkItemSelectionModel diff --git a/src/kconcatenaterowsproxymodel.cpp b/src/kconcatenaterowsproxymodel.cpp new file mode 100644 index 0000000..32b37f2 --- /dev/null +++ b/src/kconcatenaterowsproxymodel.cpp @@ -0,0 +1,354 @@ +/* + Copyright (c) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + Authors: David Faure + + 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 &roles); + void slotModelAboutToBeReset(); + void slotModelReset(); + + KConcatenateRowsProxyModel *q; + QList 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(sourceIndex.model()); + return sourceModel->setData(sourceIndex, value, role); +} + +QMap 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)), this, SLOT(slotDataChanged(QModelIndex,QModelIndex,QVector))); + 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, QAbstractItemModel::LayoutChangeHint)), + this, SIGNAL(layoutAboutToBeChanged(QList, QAbstractItemModel::LayoutChangeHint))); + connect(sourceModel, SIGNAL(layoutChanged(QList, QAbstractItemModel::LayoutChangeHint)), + this, SIGNAL(layoutChanged(QList, 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(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(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(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(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(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(q->sender()); + if (m_models.at(0) == model) { + q->endRemoveColumns(); + } +} + +void KConcatenateRowsProxyModelPrivate::slotDataChanged(const QModelIndex &from, const QModelIndex &to, const QVector &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(q->sender()); + Q_ASSERT(m_models.contains(const_cast(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(q->sender()); + Q_ASSERT(m_models.contains(const_cast(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" diff --git a/src/kconcatenaterowsproxymodel.h b/src/kconcatenaterowsproxymodel.h new file mode 100644 index 0000000..82f28ff --- /dev/null +++ b/src/kconcatenaterowsproxymodel.h @@ -0,0 +1,149 @@ +/* + Copyright (c) 2015 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + Authors: David Faure + + 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 +#include +#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 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 &roles)) + Q_PRIVATE_SLOT(d, void slotModelAboutToBeReset()) + Q_PRIVATE_SLOT(d, void slotModelReset()) + +private: + friend class KConcatenateRowsProxyModelPrivate; + const QScopedPointer d; +}; + +#endif