diff --git a/autotests/krearrangecolumnsproxymodeltest.cpp b/autotests/krearrangecolumnsproxymodeltest.cpp index 7ff621f..a84c8a2 100644 --- a/autotests/krearrangecolumnsproxymodeltest.cpp +++ b/autotests/krearrangecolumnsproxymodeltest.cpp @@ -1,229 +1,239 @@ /* 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 #include #include #include #include #include #include #include "test_model_helpers.h" using namespace TestModelHelpers; Q_DECLARE_METATYPE(QModelIndex) class tst_KRearrangeColumnsProxyModel : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { qRegisterMetaType(); } void init() { // Prepare the source model to use later on mod.clear(); mod.appendRow(makeStandardItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C") << QStringLiteral("D") << QStringLiteral("E"))); mod.item(0, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("m") << QStringLiteral("n") << QStringLiteral("o") << QStringLiteral("p") << QStringLiteral("-"))); mod.item(0, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("q") << QStringLiteral("r") << QStringLiteral("s") << QStringLiteral("t") << QStringLiteral("-"))); mod.appendRow(makeStandardItems(QStringList() << QStringLiteral("E") << QStringLiteral("F") << QStringLiteral("G") << QStringLiteral("H") << QStringLiteral("I"))); mod.item(1, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("x") << QStringLiteral("y") << QStringLiteral("z") << QStringLiteral(".") << QStringLiteral("-"))); mod.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3") << QStringLiteral("H4") << QStringLiteral("H5")); QCOMPARE(extractRowTexts(&mod, 0), QStringLiteral("ABCDE")); QCOMPARE(extractRowTexts(&mod, 0, mod.index(0, 0)), QStringLiteral("mnop-")); QCOMPARE(extractRowTexts(&mod, 1, mod.index(0, 0)), QStringLiteral("qrst-")); QCOMPARE(extractRowTexts(&mod, 1), QStringLiteral("EFGHI")); QCOMPARE(extractRowTexts(&mod, 0, mod.index(1, 0)), QStringLiteral("xyz.-")); QCOMPARE(extractHorizontalHeaderTexts(&mod), QStringLiteral("H1H2H3H4H5")); // test code to see the model // showModel(&mod); } + void shouldShowNothingIfNoSourceModel() + { + // Given a rearrange-columns proxy with no source model + KRearrangeColumnsProxyModel pm; + + // Then the proxy should show nothing (no columns selected) + QCOMPARE(pm.rowCount(), 0); + QCOMPARE(pm.columnCount(), 0); + } + void shouldShowNothingIfNoColumnSelection() { // Given a rearrange-columns proxy KRearrangeColumnsProxyModel pm; // When setting it to a source model pm.setSourceModel(&mod); // Then the proxy should show nothing (no columns selected) QCOMPARE(pm.rowCount(), mod.rowCount()); QCOMPARE(pm.columnCount(), 0); } void shouldShowNothingIfNoRows() { // Given a rearrange-columns proxy KRearrangeColumnsProxyModel pm; pm.setSourceColumns(QVector() << 2 << 3 << 1 << 0); // When using that proxy on top of an empty source model QStandardItemModel sourceModel; sourceModel.setColumnCount(4); pm.setSourceModel(&sourceModel); // Then the proxy should show nothing QCOMPARE(pm.rowCount(), 0); QCOMPARE(pm.columnCount(), 4); QCOMPARE(pm.index(0, 0), pm.index(0, 0)); // like QAbstractItemView::setModel does in a Q_ASSERT_X } void shouldRearrangeColumns() { // Given a rearrange-columns proxy KRearrangeColumnsProxyModel pm; // When setting it to a source model, with columns rearranged setup(pm); // Then the proxy should show columns reordered QCOMPARE(pm.rowCount(), 2); // (verify that the mapFromSource(mapToSource(x)) == x roundtrip works) for (int row = 0; row < pm.rowCount(); ++row) { for (int col = 0; col < pm.columnCount(); ++col) { //qDebug() << "row" << row << "col" << col; QCOMPARE(pm.mapFromSource(pm.mapToSource(pm.index(row, col))), pm.index(row, col)); } } QCOMPARE(indexRowCol(pm.index(0, 0)), QStringLiteral("0,0")); QCOMPARE(pm.rowCount(pm.index(0, 0)), 2); QCOMPARE(pm.index(0, 0).parent(), QModelIndex()); QCOMPARE(pm.mapToSource(pm.index(0, 0)).column(), 2); // column 0 points to C QCOMPARE(pm.mapToSource(pm.index(0, 1)).column(), 3); // column 1 points to D QCOMPARE(pm.sibling(0, 1, pm.index(0, 0)).column(), 1); QCOMPARE(pm.sibling(0, 0, pm.index(0, 1)).column(), 0); QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("CDBA")); QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("opnm")); QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("strq")); QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("GHFE")); QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("z.yx")); QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H3H4H2H1")); // Verify tree structure of proxy const QModelIndex secondParent = pm.index(1, 0); QVERIFY(!secondParent.parent().isValid()); QCOMPARE(indexToText(pm.index(0, 0, secondParent).parent()), indexToText(secondParent)); QCOMPARE(indexToText(pm.index(0, 3, secondParent).parent()), indexToText(secondParent)); QVERIFY(!pm.canFetchMore(QModelIndex())); } void shouldHandleDataChanged() { // Given a rearrange-columns proxy KRearrangeColumnsProxyModel pm; setup(pm); QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex))); // When a cell in a source model changes mod.item(0, 2)->setData("c", Qt::EditRole); mod.item(0, 3)->setData("d", Qt::EditRole); // Then the change should be notified to the proxy QCOMPARE(dataChangedSpy.count(), 2); QCOMPARE(indexToText(dataChangedSpy.at(0).at(0).toModelIndex()), indexToText(pm.index(0, 0))); QCOMPARE(indexToText(dataChangedSpy.at(1).at(0).toModelIndex()), indexToText(pm.index(0, 1))); QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("cdBA")); } void shouldHandleDataChangedInChild() { // Given a rearrange-columns proxy KRearrangeColumnsProxyModel pm; setup(pm); QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex))); // When a cell in a source model changes mod.item(1, 0)->child(0, 3)->setData(",", Qt::EditRole); // Then the change should be notified to the proxy QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(indexToText(dataChangedSpy.at(0).at(0).toModelIndex()), indexToText(pm.index(1, 0).child(0, 1))); QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("z,yx")); } void shouldSupportSetData() { // Given a rearrange-columns proxy KRearrangeColumnsProxyModel pm; setup(pm); QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex))); // When changing data via the proxy const QModelIndex idx = pm.index(0, 2); QCOMPARE(idx.data().toString(), QStringLiteral("B")); pm.setData(idx, QStringLiteral("Z")); QCOMPARE(idx.data().toString(), QStringLiteral("Z")); QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("CDZA")); QCOMPARE(extractRowTexts(&mod, 0), QStringLiteral("AZCDE")); } private: // setup proxy void setup(KRearrangeColumnsProxyModel &pm) { pm.setSourceColumns(QVector() << 2 << 3 << 1 << 0); pm.setSourceModel(&mod); pm.sort(0); // don't forget this! } static QString indexRowCol(const QModelIndex &index) { if (!index.isValid()) { return QStringLiteral("invalid"); } return QString::number(index.row()) + "," + QString::number(index.column()); } static QString indexToText(const QModelIndex &index) { if (!index.isValid()) { return QStringLiteral("invalid"); } return QString::number(index.row()) + "," + QString::number(index.column()) + "," + QString::number(reinterpret_cast(index.internalPointer()), 16) + " in " + QString::number(reinterpret_cast(index.model()), 16); } QStandardItemModel mod; }; QTEST_MAIN(tst_KRearrangeColumnsProxyModel) #include "krearrangecolumnsproxymodeltest.moc" diff --git a/src/krearrangecolumnsproxymodel.cpp b/src/krearrangecolumnsproxymodel.cpp index 5925809..3f5eede 100644 --- a/src/krearrangecolumnsproxymodel.cpp +++ b/src/krearrangecolumnsproxymodel.cpp @@ -1,139 +1,145 @@ /* 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 "krearrangecolumnsproxymodel.h" class KRearrangeColumnsProxyModelPrivate { public: QVector m_sourceColumns; }; KRearrangeColumnsProxyModel::KRearrangeColumnsProxyModel(QObject *parent) : QIdentityProxyModel(parent), d_ptr(new KRearrangeColumnsProxyModelPrivate) { } KRearrangeColumnsProxyModel::~KRearrangeColumnsProxyModel() { } void KRearrangeColumnsProxyModel::setSourceColumns(const QVector &columns) { d_ptr->m_sourceColumns = columns; } int KRearrangeColumnsProxyModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); + if (!sourceModel()) { + return 0; + } return d_ptr->m_sourceColumns.count(); } int KRearrangeColumnsProxyModel::rowCount(const QModelIndex &parent) const { Q_ASSERT(parent.isValid() ? parent.model() == this : true); + if (!sourceModel()) { + return 0; + } // The parent in the source model is on column 0, whatever swapping we are doing const QModelIndex sourceParent = mapToSource(parent).sibling(parent.row(), 0); return sourceModel()->rowCount(sourceParent); } // We derive from QIdentityProxyModel simply to be able to use // its mapFromSource method which has friend access to createIndex() in the source model. QModelIndex KRearrangeColumnsProxyModel::index(int row, int column, const QModelIndex &parent) const { Q_ASSERT(parent.isValid() ? parent.model() == this : true); Q_ASSERT(row >= 0); Q_ASSERT(column >= 0); // The parent in the source model is on column 0, whatever swapping we are doing const QModelIndex sourceParent = mapToSource(parent).sibling(parent.row(), 0); // Find the child in the source model, we need its internal pointer const QModelIndex sourceIndex = sourceModel()->index(row, sourceColumnForProxyColumn(column), sourceParent); if (!sourceIndex.isValid()) { return QModelIndex(); } return createIndex(row, column, sourceIndex.internalPointer()); } QModelIndex KRearrangeColumnsProxyModel::parent(const QModelIndex &child) const { Q_ASSERT(child.isValid() ? child.model() == this : true); const QModelIndex sourceIndex = mapToSource(child); const QModelIndex sourceParent = sourceIndex.parent(); if (!sourceParent.isValid()) { return QModelIndex(); } return createIndex(sourceParent.row(), 0, sourceParent.internalPointer()); } QVariant KRearrangeColumnsProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { const int sourceCol = sourceColumnForProxyColumn(section); return sourceModel()->headerData(sourceCol, orientation, role); } else { return QIdentityProxyModel::headerData(section, orientation, role); } } QModelIndex KRearrangeColumnsProxyModel::sibling(int row, int column, const QModelIndex &idx) const { if (column >= d_ptr->m_sourceColumns.count()) { return QModelIndex(); } return index(row, column, idx.parent()); } QModelIndex KRearrangeColumnsProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) { return QModelIndex(); } Q_ASSERT(sourceIndex.model() == sourceModel()); const int proxyColumn = proxyColumnForSourceColumn(sourceIndex.column()); return createIndex(sourceIndex.row(), proxyColumn, sourceIndex.internalPointer()); } QModelIndex KRearrangeColumnsProxyModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid()) { return QModelIndex(); } // This is just an indirect way to call sourceModel->createIndex(row, sourceColumn, pointer) const QModelIndex fakeIndex = createIndex(proxyIndex.row(), sourceColumnForProxyColumn(proxyIndex.column()), proxyIndex.internalPointer()); return QIdentityProxyModel::mapToSource(fakeIndex); } int KRearrangeColumnsProxyModel::proxyColumnForSourceColumn(int sourceColumn) const { // If this is too slow, we could add a second QVector with index=logical_source_column value=desired_pos_in_proxy. return d_ptr->m_sourceColumns.indexOf(sourceColumn); } int KRearrangeColumnsProxyModel::sourceColumnForProxyColumn(int proxyColumn) const { Q_ASSERT(proxyColumn >= 0); Q_ASSERT(proxyColumn < d_ptr->m_sourceColumns.size()); return d_ptr->m_sourceColumns.at(proxyColumn); }