diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 4e604ee..f98e87d 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -20,6 +20,7 @@ ecm_add_tests( testmodelqueuedconnections.cpp kselectionproxymodeltest.cpp krecursivefilterproxymodeltest.cpp + krearrangecolumnsproxymodeltest.cpp LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Widgets proxymodeltestsuite ) diff --git a/autotests/krearrangecolumnsproxymodeltest.cpp b/autotests/krearrangecolumnsproxymodeltest.cpp new file mode 100644 index 0000000..0e52e61 --- /dev/null +++ b/autotests/krearrangecolumnsproxymodeltest.cpp @@ -0,0 +1,209 @@ +/* + 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() << "A" << "B" << "C" << "D" << "E")); + mod.item(0, 0)->appendRow(makeStandardItems(QStringList() << "m" << "n" << "o" << "p" << "-")); + mod.item(0, 0)->appendRow(makeStandardItems(QStringList() << "q" << "r" << "s" << "t" << "-")); + mod.appendRow(makeStandardItems(QStringList() << "E" << "F" << "G" << "H" << "I")); + mod.item(1, 0)->appendRow(makeStandardItems(QStringList() << "x" << "y" << "z" << "." << "-")); + mod.setHorizontalHeaderLabels(QStringList() << "H1" << "H2" << "H3" << "H4" << "H5"); + + QCOMPARE(extractRowTexts(&mod, 0), QString("ABCDE")); + QCOMPARE(extractRowTexts(&mod, 0, mod.index(0, 0)), QString("mnop-")); + QCOMPARE(extractRowTexts(&mod, 1, mod.index(0, 0)), QString("qrst-")); + QCOMPARE(extractRowTexts(&mod, 1), QString("EFGHI")); + QCOMPARE(extractRowTexts(&mod, 0, mod.index(1, 0)), QString("xyz.-")); + QCOMPARE(extractHorizontalHeaderTexts(&mod), QString("H1H2H3H4H5")); + + // test code to see the model + // showModel(&mod); + } + + 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 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)), QString("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(extractRowTexts(&pm, 0), QString("CDBA")); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QString("opnm")); + QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QString("strq")); + QCOMPARE(extractRowTexts(&pm, 1), QString("GHFE")); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QString("z.yx")); + QCOMPARE(extractHorizontalHeaderTexts(&pm), QString("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).value()), indexToText(pm.index(0, 0))); + QCOMPARE(indexToText(dataChangedSpy.at(1).at(0).value()), indexToText(pm.index(0, 1))); + QCOMPARE(extractRowTexts(&pm, 0), QString("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).value()), indexToText(pm.index(1, 0).child(0, 1))); + QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QString("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(), QString("B")); + pm.setData(idx, QString("Z")); + QCOMPARE(idx.data().toString(), QString("Z")); + QCOMPARE(extractRowTexts(&pm, 0), QString("CDZA")); + QCOMPARE(extractRowTexts(&mod, 0), QString("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 "invalid"; + } + return QString::number(index.row()) + "," + QString::number(index.column()); + } + + static QString indexToText(const QModelIndex &index) + { + if (!index.isValid()) { + return "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/autotests/test_model_helpers.h b/autotests/test_model_helpers.h new file mode 100644 index 0000000..a44db8a --- /dev/null +++ b/autotests/test_model_helpers.h @@ -0,0 +1,62 @@ +/* + 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 + +namespace TestModelHelpers +{ + +// Prepares one row for a QStandardItemModel +inline QList makeStandardItems(const QStringList &texts) +{ + QList items; + foreach (const QString &txt, texts) { + items << new QStandardItem(txt); + } + return items; +} + +// Extracts a full row from a model as a string +// Works best if every cell contains only one character +inline QString extractRowTexts(QAbstractItemModel *model, int row, const QModelIndex &parent = QModelIndex()) +{ + QString result; + const int colCount = model->columnCount(); + for (int col = 0; col < colCount; ++col) { + const QString txt = model->index(row, col, parent).data().toString(); + result += txt.isEmpty() ? QString::fromLatin1(" ") : txt; + } + return result; +} + +// Extracts all headers +inline QString extractHorizontalHeaderTexts(QAbstractItemModel *model) +{ + QString result; + const int colCount = model->columnCount(); + for (int col = 0; col < colCount; ++col) { + const QString txt = model->headerData(col, Qt::Horizontal).toString(); + result += txt.isEmpty() ? QString::fromLatin1(" ") : txt; + } + return result; +} + +} + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index dc7d5f0..82d776f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ set(kitemmodels_SRCS kdescendantsproxymodel.cpp klinkitemselectionmodel.cpp kmodelindexproxymapper.cpp + krearrangecolumnsproxymodel.cpp krecursivefilterproxymodel.cpp kselectionproxymodel.cpp ) @@ -27,6 +28,7 @@ ecm_generate_headers(KItemModels_HEADERS KBreadcrumbSelectionModel KCheckableProxyModel KLinkItemSelectionModel + KRearrangeColumnsProxyModel KRecursiveFilterProxyModel KDescendantsProxyModel KModelIndexProxyMapper diff --git a/src/krearrangecolumnsproxymodel.cpp b/src/krearrangecolumnsproxymodel.cpp new file mode 100644 index 0000000..bd1b49b --- /dev/null +++ b/src/krearrangecolumnsproxymodel.cpp @@ -0,0 +1,129 @@ +/* + 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); + return d_ptr->m_sourceColumns.count(); +} + +int KRearrangeColumnsProxyModel::rowCount(const QModelIndex &parent) const +{ + Q_ASSERT(parent.isValid() ? parent.model() == this : true); + // 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); + Q_ASSERT(sourceIndex.isValid()); + + 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::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); +} diff --git a/src/krearrangecolumnsproxymodel.h b/src/krearrangecolumnsproxymodel.h new file mode 100644 index 0000000..12ed8a6 --- /dev/null +++ b/src/krearrangecolumnsproxymodel.h @@ -0,0 +1,96 @@ +/* + 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 REARRANGECOLUMNSPROXYMODEL_H +#define REARRANGECOLUMNSPROXYMODEL_H + +#include +#include +#include "kitemmodels_export.h" + +class KRearrangeColumnsProxyModelPrivate; + +/** + * This proxy shows specific columns from the source model, in any order. + * This allows to reorder columns, as well as not showing all of them. + * + * The proxy supports source models that have a tree structure. + * It also supports editing, and propagating changes from the source model. + * + * Showing the same source column more than once is not supported. + * + * Author: David Faure, KDAB + * @since 5.12 + */ +class KITEMMODELS_EXPORT KRearrangeColumnsProxyModel : public QIdentityProxyModel +{ + Q_OBJECT +public: + /** + * Creates a KRearrangeColumnsProxyModel proxy. + * Remember to call setSourceModel afterwards. + */ + explicit KRearrangeColumnsProxyModel(QObject *parent = 0); + /** + * Destructor. + */ + ~KRearrangeColumnsProxyModel(); + + // API + + /** + * Set the chosen source columns, in the desired order for the proxy columns + * columns[proxyColumn=0] is the source column to show in the first proxy column, etc. + * + * Example: QVector() << 2 << 1; + * This examples configures the proxy to hide column 0, show column 2 from the source model, + * then show column 1 from the source model. + */ + void setSourceColumns(const QVector &columns); + + // Implementation + + /// @reimp + int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + /// @reimp + int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + + /// @reimp + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + /// @reimp + QModelIndex parent(const QModelIndex &child) const Q_DECL_OVERRIDE; + + /// @reimp + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const Q_DECL_OVERRIDE; + /// @reimp + QModelIndex mapToSource(const QModelIndex &proxyIndex) const Q_DECL_OVERRIDE; + + /// @reimp + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + +private: + int proxyColumnForSourceColumn(int sourceColumn) const; + int sourceColumnForProxyColumn(int proxyColumn) const; + +private: + const QScopedPointer d_ptr; +}; + +#endif