New proxymodel: KConcatenateRowsProxyModel

This proxy takes multiple source models and concatenates their rows.

REVIEW: 124644
wilder
David Faure 11 years ago
parent 06a48c672a
commit fe0b5826b1
  1. 1
      README.md
  2. 1
      autotests/CMakeLists.txt
  3. 405
      autotests/kconcatenaterowsproxymodeltest.cpp
  4. 2
      src/CMakeLists.txt
  5. 354
      src/kconcatenaterowsproxymodel.cpp
  6. 149
      src/kconcatenaterowsproxymodel.h

@ -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

@ -15,6 +15,7 @@ add_subdirectory(proxymodeltestsuite)
include(ECMAddTests)
ecm_add_tests(
kconcatenaterowsproxymodeltest.cpp
kdescendantsproxymodel_smoketest.cpp
kextracolumnsproxymodeltest.cpp
klinkitemselectionmodeltest.cpp

@ -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"

@ -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

@ -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…
Cancel
Save