diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -24,6 +24,7 @@ kmodelindexproxymappertest.cpp krecursivefilterproxymodeltest.cpp krearrangecolumnsproxymodeltest.cpp + kcolumnheadersmodeltest.cpp LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Widgets proxymodeltestsuite ) diff --git a/autotests/kcolumnheadersmodeltest.cpp b/autotests/kcolumnheadersmodeltest.cpp new file mode 100644 --- /dev/null +++ b/autotests/kcolumnheadersmodeltest.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 KColumnHeadersModelTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testStatic() + { + auto model = new KColumnHeadersModel{}; + + 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 KColumnHeadersModel{}; + 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 KColumnHeadersModel{}; + + 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(KColumnHeadersModelTest) + +#include "kcolumnheadersmodeltest.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 + kcolumnheadersmodel.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 + KColumnHeadersModel REQUIRED_HEADERS KItemModels_HEADERS ) @@ -75,6 +77,7 @@ kdescendantsproxymodel.h kmodelindexproxymapper.h kselectionproxymodel.h + kcolumnheadersmodel.h ) endif() diff --git a/src/core/kcolumnheadersmodel.h b/src/core/kcolumnheadersmodel.h new file mode 100644 --- /dev/null +++ b/src/core/kcolumnheadersmodel.h @@ -0,0 +1,63 @@ +/* + 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 +#include + +class KColumnHeadersModelPrivate; + +/** + * 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 KColumnHeadersModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged) + +public: + explicit KColumnHeadersModel(QObject *parent = nullptr); + ~KColumnHeadersModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex{}) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + QAbstractItemModel *sourceModel() const; + void setSourceModel(QAbstractItemModel *newSourceModel); + Q_SIGNAL void sourceModelChanged(); + +private: + const std::unique_ptr d; +}; + +#endif diff --git a/src/core/kcolumnheadersmodel.cpp b/src/core/kcolumnheadersmodel.cpp new file mode 100644 --- /dev/null +++ b/src/core/kcolumnheadersmodel.cpp @@ -0,0 +1,102 @@ +/* + 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 "kcolumnheadersmodel.h" + +class KColumnHeadersModelPrivate +{ +public: + QAbstractItemModel *sourceModel = nullptr; +}; + +KColumnHeadersModel::KColumnHeadersModel(QObject *parent) + : QAbstractListModel(parent), d(new KColumnHeadersModelPrivate) +{ +} + +KColumnHeadersModel::~KColumnHeadersModel() +{ +} + +int KColumnHeadersModel::rowCount(const QModelIndex& parent) const +{ + if (!d->sourceModel || parent.isValid()) { + return 0; + } + + return d->sourceModel->columnCount(); +} + +QVariant KColumnHeadersModel::data(const QModelIndex& index, int role) const +{ + if (!d->sourceModel || !index.isValid()) { + return QVariant{}; + } + + return sourceModel()->headerData(index.row(), Qt::Horizontal, role); +} + +QHash KColumnHeadersModel::roleNames() const +{ + if (!d->sourceModel) { + return QHash{}; + } + + return d->sourceModel->roleNames(); +} + +QAbstractItemModel *KColumnHeadersModel::sourceModel() const +{ + return d->sourceModel; +} + +void KColumnHeadersModel::setSourceModel(QAbstractItemModel* newSourceModel) +{ + if (newSourceModel == d->sourceModel) { + return; + } + + if (d->sourceModel) { + d->sourceModel->disconnect(this); + } + + beginResetModel(); + d->sourceModel = newSourceModel; + endResetModel(); + + 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, "KColumnHeadersModel"); } - - 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 + } +} +