diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -24,7 +24,7 @@ kmodelindexproxymappertest.cpp krecursivefilterproxymodeltest.cpp krearrangecolumnsproxymodeltest.cpp - LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Widgets proxymodeltestsuite + kcolumnheadersproxymodeltest.cpp ) #we need additional sources for this test, can't use it in ecm_add_tests diff --git a/autotests/kcolumnheadersproxymodeltest.cpp b/autotests/kcolumnheadersproxymodeltest.cpp new file mode 100644 --- /dev/null +++ b/autotests/kcolumnheadersproxymodeltest.cpp @@ -0,0 +1,189 @@ +/* + Copyright (c) 2019 Arjen Hiemstra + + 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 + +class KColumnHeadersProxyModelTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testStatic() + { + auto model = new KColumnHeadersProxyModel{}; + + auto sourceModel = new QStandardItemModel{}; + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 1"), + QStringLiteral("Test 2"), + QStringLiteral("Test 3"), + QStringLiteral("Test 4"), + QStringLiteral("Test 5") + }); + + model->setSourceModel(sourceModel); + + auto tester = new QAbstractItemModelTester(model); + Q_UNUSED(tester); + + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + QCOMPARE(model->data(model->index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 4")); + QCOMPARE(model->data(model->index(4, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 5")); + + QSignalSpy spy{model, &QAbstractItemModel::dataChanged}; + QVERIFY(spy.isValid()); + + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 5"), + QStringLiteral("Test 4"), + QStringLiteral("Test 3"), + QStringLiteral("Test 2"), + QStringLiteral("Test 1") + }); + + QCOMPARE(spy.count(), 4); + } + + void testAddColumns() + { + auto model = new KColumnHeadersProxyModel{}; + auto sourceModel = new QStandardItemModel{}; + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 1"), + QStringLiteral("Test 2") + }); + model->setSourceModel(sourceModel); + + auto tester = new QAbstractItemModelTester(model); + Q_UNUSED(tester); + + QSignalSpy spy{model, &QAbstractItemModel::rowsInserted}; + QVERIFY(spy.isValid()); + + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 1"), + QStringLiteral("Test 2"), + QStringLiteral("Test 3") + }); + + QCOMPARE(spy.count(), 1); + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 1"), + QStringLiteral("Test 2"), + QStringLiteral("Test 3"), + QStringLiteral("Test 4"), + QStringLiteral("Test 5") + }); + + QCOMPARE(spy.count(), 2); + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + QCOMPARE(model->data(model->index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 4")); + QCOMPARE(model->data(model->index(4, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 5")); + + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 1"), + QStringLiteral("Test 2"), + QStringLiteral("Test 6"), + QStringLiteral("Test 3"), + QStringLiteral("Test 4"), + QStringLiteral("Test 5") + }); + + QCOMPARE(spy.count(), 3); + QCOMPARE(model->rowCount(), 6); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 6")); + QCOMPARE(model->data(model->index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + QCOMPARE(model->data(model->index(4, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 4")); + QCOMPARE(model->data(model->index(5, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 5")); + } + + void testRemoveColumns() + { + auto model = new KColumnHeadersProxyModel{}; + + auto sourceModel = new QStandardItemModel{}; + sourceModel->setHorizontalHeaderLabels({ + QStringLiteral("Test 1"), + QStringLiteral("Test 2"), + QStringLiteral("Test 3"), + QStringLiteral("Test 4"), + QStringLiteral("Test 5") + }); + + model->setSourceModel(sourceModel); + + auto tester = new QAbstractItemModelTester(model); + Q_UNUSED(tester); + + QSignalSpy spy{model, &QAbstractItemModel::rowsRemoved}; + QVERIFY(spy.isValid()); + + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + QCOMPARE(model->data(model->index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 4")); + QCOMPARE(model->data(model->index(4, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 5")); + + sourceModel->takeColumn(4); + + QCOMPARE(spy.count(), 1); + + QCOMPARE(model->rowCount(), 4); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 2")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + QCOMPARE(model->data(model->index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 4")); + + sourceModel->takeColumn(1); + + QCOMPARE(spy.count(), 2); + + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 1")); + QCOMPARE(model->data(model->index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 3")); + QCOMPARE(model->data(model->index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Test 4")); + } +}; + +QTEST_MAIN(KColumnHeadersProxyModelTest) + +#include "kcolumnheadersproxymodeltest.moc" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -10,6 +10,7 @@ krearrangecolumnsproxymodel.cpp krecursivefilterproxymodel.cpp kselectionproxymodel.cpp + kcolumnheadersproxymodel.cpp ) ecm_qt_declare_logging_category(kitemmodels_SRCS HEADER kitemmodels_debug.h IDENTIFIER KITEMMODELS_LOG CATEGORY_NAME kf5.kitemmodels) @@ -51,6 +52,7 @@ KDescendantsProxyModel KModelIndexProxyMapper KSelectionProxyModel + KColumnHeadersProxyModel REQUIRED_HEADERS KItemModels_HEADERS ) @@ -75,6 +77,7 @@ kdescendantsproxymodel.h kmodelindexproxymapper.h kselectionproxymodel.h + kcolumnheadersproxymodel.h ) endif() diff --git a/src/core/kcolumnheadersproxymodel.h b/src/core/kcolumnheadersproxymodel.h new file mode 100644 --- /dev/null +++ b/src/core/kcolumnheadersproxymodel.h @@ -0,0 +1,59 @@ +/* + Copyright (c) 2019 Arjen Hiemstra + + 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 KCOLUMNHEADERSMODEL_H +#define KCOLUMNHEADERSMODEL_H + +#include "kitemmodels_export.h" + +#include + +/** + * A model that converts a model's headers into a list model. + * + * This model will expose the source model's headers as a simple list. This is + * mostly useful as a helper for QML applications that want to display a model's + * headers. + * + * Each columns's header will be presented as a row in this model. Roles are + * forwarded directly to the source model's headerData() method. + * + * @since 5.65 + */ +class KITEMMODELS_EXPORT KColumnHeadersProxyModel : public QAbstractProxyModel +{ + Q_OBJECT +public: + explicit KColumnHeadersProxyModel(QObject *parent = nullptr); + ~KColumnHeadersProxyModel() override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex{}) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = QModelIndex{}) const override; + int columnCount(const QModelIndex &parent = QModelIndex{}) const override; + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + + QVariant data(const QModelIndex &index, int role) const override; + + void setSourceModel(QAbstractItemModel *newSourceModel) override; +}; + +#endif diff --git a/src/core/kcolumnheadersproxymodel.cpp b/src/core/kcolumnheadersproxymodel.cpp new file mode 100644 --- /dev/null +++ b/src/core/kcolumnheadersproxymodel.cpp @@ -0,0 +1,114 @@ +/* + Copyright (c) 2019 Arjen Hiemstra + + 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 "kcolumnheadersproxymodel.h" + +KColumnHeadersProxyModel::KColumnHeadersProxyModel(QObject *parent) + : QAbstractProxyModel(parent) +{ +} + +KColumnHeadersProxyModel::~KColumnHeadersProxyModel() +{ +} + +QModelIndex KColumnHeadersProxyModel::index(int row, int column, const QModelIndex& parent) const +{ + if (!sourceModel() || parent.isValid()) { + return QModelIndex{}; + } + + if (column != 0 || row < 0 || row > rowCount() - 1) { + return QModelIndex{}; + } + + return createIndex(row, column); +} + +int KColumnHeadersProxyModel::rowCount(const QModelIndex& parent) const +{ + if (!sourceModel() || parent.isValid()) { + return 0; + } + + return sourceModel()->columnCount(); +} + +int KColumnHeadersProxyModel::columnCount(const QModelIndex& parent) const +{ + if (!sourceModel() || parent.isValid()) { + return 0; + } + + return 1; +} + +QModelIndex KColumnHeadersProxyModel::parent(const QModelIndex& child) const +{ + Q_UNUSED(child); + return QModelIndex{}; +} + +QModelIndex KColumnHeadersProxyModel::mapFromSource(const QModelIndex& sourceIndex) const +{ + return index(sourceIndex.column(), 0); +} + +QModelIndex KColumnHeadersProxyModel::mapToSource(const QModelIndex& proxyIndex) const +{ + return sourceModel()->index(0, proxyIndex.column()); +} + +QVariant KColumnHeadersProxyModel::data(const QModelIndex& index, int role) const +{ + if (!sourceModel() || !index.isValid()) { + return QVariant{}; + } + + return sourceModel()->headerData(index.row(), Qt::Horizontal, role); +} + +void KColumnHeadersProxyModel::setSourceModel(QAbstractItemModel* newSourceModel) +{ + if (sourceModel()) { + sourceModel()->disconnect(this); + } + + QAbstractProxyModel::setSourceModel(newSourceModel); + + if (newSourceModel) { + connect(newSourceModel, &QAbstractItemModel::columnsAboutToBeInserted, this, [this](const QModelIndex&, int first, int last) { + beginInsertRows(QModelIndex{}, first, last); + }); + connect(newSourceModel, &QAbstractItemModel::columnsInserted, this, [this]() { endInsertRows(); }); + connect(newSourceModel, &QAbstractItemModel::columnsAboutToBeMoved, this, [this](const QModelIndex&, int start, int end, const QModelIndex&, int destination) { + beginMoveRows(QModelIndex{}, start, end, QModelIndex{}, destination); + }); + connect(newSourceModel, &QAbstractItemModel::columnsMoved, this, [this]() { endMoveRows(); }); + connect(newSourceModel, &QAbstractItemModel::columnsAboutToBeRemoved, this, [this](const QModelIndex &, int first, int last) { + beginRemoveRows(QModelIndex{}, first, last); + }); + connect(newSourceModel, &QAbstractItemModel::columnsRemoved, this, [this]() { endRemoveRows(); }); + connect(newSourceModel, &QAbstractItemModel::headerDataChanged, this, [this](Qt::Orientation orientation, int first, int last) { + if (orientation == Qt::Horizontal) { + Q_EMIT dataChanged(index(first, 0), index(last, 0)); + } + }); + } +} diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp --- a/src/qml/plugin.cpp +++ b/src/qml/plugin.cpp @@ -23,6 +23,7 @@ #include #include +#include #include "kconcatenaterowsproxymodel_qml.h" void Plugin::initializeEngine(QQmlEngine *engine, const char *uri) @@ -34,6 +35,5 @@ qmlRegisterType(); qmlRegisterExtendedType(uri, 1, 0, "KConcatenateRowsProxyModel"); qmlRegisterType(uri, 1, 0, "KDescendantsProxyModel"); + qmlRegisterType(uri, 1, 0, "KColumnHeadersProxyModel"); } - - diff --git a/tests/qml/columnheaders.qml b/tests/qml/columnheaders.qml new file mode 100644 --- /dev/null +++ b/tests/qml/columnheaders.qml @@ -0,0 +1,21 @@ +import QtQuick 2.0 +import org.kde.kitemmodels 1.0 + +// This test is somewhat lame in that it should only display a single "1". +// ListModel only has one column and no way of adding more. And there is not +// really another model that is simple to create from QML that does have columns +// and column headers. + +ListView { + model: KColumnHeadersProxyModel { + sourceModel: ListModel { + ListElement { display: "test1" } + ListElement { display: "test2" } + } + } + + delegate: Text { + text: model.display + } +} +