diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 79c36b5..719e56c 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,42 +1,43 @@ set(proxyModelSmokeTestSources kselectionproxymodeltestsuite.cpp ) remove_definitions(-DQT_NO_CAST_TO_ASCII) remove_definitions(-DQT_NO_CAST_FROM_ASCII) remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test Widgets) add_subdirectory(proxymodeltestsuite) include(ECMAddTests) ecm_add_tests( kconcatenaterowsproxymodeltest.cpp kdescendantsproxymodel_smoketest.cpp kdescendantsproxymodeltest.cpp kextracolumnsproxymodeltest.cpp klinkitemselectionmodeltest.cpp kselectionproxymodeltest.cpp kmodelindexproxymappertest.cpp krecursivefilterproxymodeltest.cpp krearrangecolumnsproxymodeltest.cpp knumbermodeltest.cpp + kcolumnheadersmodeltest.cpp LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Widgets proxymodeltestsuite ) if (${Qt5Qml_FOUND}) ecm_add_tests( kconcatenaterows_qml.cpp LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Qml ) endif() #we need additional sources for this test, can't use it in ecm_add_tests ecm_add_test(kselectionproxymodel_smoketest.cpp ${proxyModelSmokeTestSources} TEST_NAME "kselectionproxymodel_smoketest" LINK_LIBRARIES KF5::ItemModels Qt5::Test Qt5::Widgets proxymodeltestsuite ) diff --git a/autotests/kcolumnheadersmodeltest.cpp b/autotests/kcolumnheadersmodeltest.cpp new file mode 100644 index 0000000..3e2e252 --- /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 index 44c57b5..ad5d262 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1,116 +1,119 @@ set(kitemmodels_SRCS kbreadcrumbselectionmodel.cpp kcheckableproxymodel.cpp kconcatenaterowsproxymodel.cpp kdescendantsproxymodel.cpp kextracolumnsproxymodel.cpp klinkitemselectionmodel.cpp kmodelindexproxymapper.cpp knumbermodel.cpp 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) add_library(KF5ItemModels ${kitemmodels_SRCS}) add_library(KF5::ItemModels ALIAS KF5ItemModels) ecm_generate_export_header(KF5ItemModels BASE_NAME KItemModels GROUP_BASE_NAME KF VERSION ${KF5_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 4.8 5.65 EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} ) # reminder trigger to apply KITEMMODELS_ENABLE_DEPRECATED_SINCE & friends to KConcatenateRowsProxyModel when the time is there if (REQUIRED_QT_VERSION VERSION_GREATER 5.12.0) message(WARNING "With Qt 5.13 as min dep, mark KConcatenateRowsProxyModel as deprecated to the compiler & remove this warning") endif() target_include_directories(KF5ItemModels INTERFACE "$") target_link_libraries(KF5ItemModels PUBLIC Qt5::Core) set_target_properties(KF5ItemModels PROPERTIES VERSION ${KITEMMODELS_VERSION_STRING} SOVERSION ${KITEMMODELS_SOVERSION} EXPORT_NAME ItemModels ) ecm_generate_headers(KItemModels_HEADERS HEADER_NAMES KBreadcrumbSelectionModel KConcatenateRowsProxyModel KCheckableProxyModel KExtraColumnsProxyModel KLinkItemSelectionModel KRearrangeColumnsProxyModel KRecursiveFilterProxyModel KDescendantsProxyModel KModelIndexProxyMapper KSelectionProxyModel KNumberModel + KColumnHeadersModel REQUIRED_HEADERS KItemModels_HEADERS ) find_package(PythonModuleGeneration) if (PythonModuleGeneration_FOUND) ecm_generate_python_binding( TARGET KF5::ItemModels PYTHONNAMESPACE PyKF5 MODULENAME KItemModels SIP_DEPENDS QtCore/QtCoremod.sip HEADERS kbreadcrumbselectionmodel.h kconcatenaterowsproxymodel.h kcheckableproxymodel.h kextracolumnsproxymodel.h klinkitemselectionmodel.h krearrangecolumnsproxymodel.h krecursivefilterproxymodel.h kdescendantsproxymodel.h kmodelindexproxymapper.h kselectionproxymodel.h + kcolumnheadersmodel.h ) endif() install(TARGETS KF5ItemModels EXPORT KF5ItemModelsTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kitemmodels_export.h ${KItemModels_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KItemModels COMPONENT Devel ) if(BUILD_QCH) ecm_add_qch( KF5ItemModels_QCH NAME KItemModels BASE_NAME KF5ItemModels VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KItemModels_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt5Core_QCH INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR} BLANK_MACROS KITEMMODELS_EXPORT "KITEMMODELS_DEPRECATED_VERSION(x, y, t)" TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KItemModels LIB_NAME KF5ItemModels DEPS "core" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KItemModels) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/core/kcolumnheadersmodel.cpp b/src/core/kcolumnheadersmodel.cpp new file mode 100644 index 0000000..c2a92d7 --- /dev/null +++ b/src/core/kcolumnheadersmodel.cpp @@ -0,0 +1,106 @@ +/* + 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)); + } + }); + connect(newSourceModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged); + connect(newSourceModel, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged); + connect(newSourceModel, &QAbstractItemModel::modelAboutToBeReset, this, [this]() { beginResetModel(); }); + connect(newSourceModel, &QAbstractItemModel::modelReset, this, [this]() { endResetModel(); }); + } +} diff --git a/src/core/kcolumnheadersmodel.h b/src/core/kcolumnheadersmodel.h new file mode 100644 index 0000000..0deda4e --- /dev/null +++ b/src/core/kcolumnheadersmodel.h @@ -0,0 +1,65 @@ +/* + 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_SIGNALS: + void sourceModelChanged(); + +private: + const std::unique_ptr d; +}; + +#endif diff --git a/src/qml/plugin.cpp b/src/qml/plugin.cpp index 447cffe..4448cef 100644 --- a/src/qml/plugin.cpp +++ b/src/qml/plugin.cpp @@ -1,47 +1,47 @@ /* Copyright (c) 2019 David Edmundson 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 "plugin.h" #include #include #include #include +#include #include "kconcatenaterowsproxymodel_qml.h" void Plugin::initializeEngine(QQmlEngine *engine, const char *uri) { Q_UNUSED(engine); Q_UNUSED(uri); } void Plugin::registerTypes(const char *uri) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) qmlRegisterAnonymousType(uri, 1); #else qmlRegisterType(); #endif qmlRegisterExtendedType(uri, 1, 0, "KConcatenateRowsProxyModel"); qmlRegisterType(uri, 1, 0, "KDescendantsProxyModel"); qmlRegisterType(uri, 1, 0, "KNumberModel"); + qmlRegisterType(uri, 1, 0, "KColumnHeadersModel"); } - - diff --git a/tests/qml/columnheaders.qml b/tests/qml/columnheaders.qml new file mode 100644 index 0000000..632180d --- /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 + } +} +