From 55b74b2bfabbb4117dc65ca7abc49ceb0f9dc4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Gr=C3=A4=C3=9Flin?= Date: Tue, 24 Jun 2014 15:49:25 +0200 Subject: [PATCH] [klipper] Introduce a HistoryModel The HistoryModel provides the needed functionality to be hooked into History. It's not setting up the chaining (that's implicit in a model) and uses shared pointer. Both needs further adjustments in Klipper. --- klipper/CMakeLists.txt | 1 + klipper/autotests/CMakeLists.txt | 21 + klipper/autotests/historymodeltest.cpp | 202 +++++++++ klipper/autotests/modeltest.cpp | 600 +++++++++++++++++++++++++ klipper/autotests/modeltest.h | 96 ++++ klipper/historymodel.cpp | 169 +++++++ klipper/historymodel.h | 59 +++ 7 files changed, 1148 insertions(+) create mode 100644 klipper/autotests/historymodeltest.cpp create mode 100644 klipper/autotests/modeltest.cpp create mode 100644 klipper/autotests/modeltest.h create mode 100644 klipper/historymodel.cpp create mode 100644 klipper/historymodel.h diff --git a/klipper/CMakeLists.txt b/klipper/CMakeLists.txt index 06ef3741f..13807bb9a 100644 --- a/klipper/CMakeLists.txt +++ b/klipper/CMakeLists.txt @@ -7,6 +7,7 @@ set(libklipper_common_SRCS configdialog.cpp history.cpp historyitem.cpp + historymodel.cpp historystringitem.cpp klipperpopup.cpp popupproxy.cpp diff --git a/klipper/autotests/CMakeLists.txt b/klipper/autotests/CMakeLists.txt index dc1fda5a3..e167c6e8d 100644 --- a/klipper/autotests/CMakeLists.txt +++ b/klipper/autotests/CMakeLists.txt @@ -19,3 +19,24 @@ target_link_libraries(testHistory ) add_test(klipper-testHistory testHistory) ecm_mark_as_test(testHistory) + +######################################################## +# Test History Model +######################################################## +set(testHistoryModel_SRCS + historymodeltest.cpp + modeltest.cpp + ../historymodel.cpp + ../historyimageitem.cpp + ../historyitem.cpp + ../historystringitem.cpp + ../historyurlitem.cpp +) +add_executable(testHistoryModel ${testHistoryModel_SRCS}) +target_link_libraries(testHistoryModel + Qt5::Test + Qt5::Widgets # QAction + KF5::CoreAddons # KUrlMimeData +) +add_test(klipper-testHistoryModel testHistoryModel) +ecm_mark_as_test(testHistoryModel) diff --git a/klipper/autotests/historymodeltest.cpp b/klipper/autotests/historymodeltest.cpp new file mode 100644 index 000000000..12081fa39 --- /dev/null +++ b/klipper/autotests/historymodeltest.cpp @@ -0,0 +1,202 @@ +/******************************************************************** +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "modeltest.h" +#include "../historymodel.h" +#include "../historystringitem.h" + +#include +Q_DECLARE_METATYPE(HistoryItem*) + +class HistoryModelTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testSetMaxSize(); + void testInsertRemove(); + void testClear(); + void testIndexOf(); +}; + +void HistoryModelTest::testSetMaxSize() +{ + QScopedPointer history(new HistoryModel(nullptr)); + QScopedPointer modelTest(new ModelTest(history.data())); + + QCOMPARE(history->rowCount(), 0); + QCOMPARE(history->maxSize(), 0); + + // insert an item - should still be empty + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foo")))); + QCOMPARE(history->rowCount(), 0); + + // now it should insert again + history->setMaxSize(1); + QCOMPARE(history->maxSize(), 1); + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foo")))); + QCOMPARE(history->rowCount(), 1); + + // insert another item, foo should get removed + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("bar")))); + QCOMPARE(history->rowCount(), 1); + QCOMPARE(history->data(history->index(0, 0)).toString(), QLatin1String("bar")); + + // I don't trust the model, add more items + history->setMaxSize(10); + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foo")))); + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foobar")))); + QCOMPARE(history->rowCount(), 3); + QCOMPARE(history->data(history->index(0, 0)).toString(), QLatin1String("foobar")); + QCOMPARE(history->data(history->index(1, 0)).toString(), QLatin1String("foo")); + QCOMPARE(history->data(history->index(2, 0)).toString(), QLatin1String("bar")); + + // setting to 0 should clear again + history->setMaxSize(0); + QCOMPARE(history->maxSize(), 0); + QCOMPARE(history->rowCount(), 0); +} + +void HistoryModelTest::testInsertRemove() +{ + QScopedPointer history(new HistoryModel(nullptr)); + QScopedPointer modelTest(new ModelTest(history.data())); + history->setMaxSize(10); + QCOMPARE(history->rowCount(), 0); + + const QString fooText = QStringLiteral("foo"); + const QString barText = QStringLiteral("bar"); + const QString fooBarText = QStringLiteral("foobar"); + const QByteArray fooUuid = QCryptographicHash::hash(fooText.toUtf8(), QCryptographicHash::Sha1); + const QByteArray barUuid = QCryptographicHash::hash(barText.toUtf8(), QCryptographicHash::Sha1); + const QByteArray foobarUuid = QCryptographicHash::hash(fooBarText.toUtf8(), QCryptographicHash::Sha1); + + // let's insert a few items + history->insert(QSharedPointer(new HistoryStringItem(fooText))); + QCOMPARE(history->data(history->index(0, 0)).toString(), fooText); +// QCOMPARE(history->first()->next_uuid(), history->first()->uuid()); +// QCOMPARE(history->first()->previous_uuid(), history->first()->uuid()); + + history->insert(QSharedPointer(new HistoryStringItem(barText))); + QCOMPARE(history->data(history->index(0, 0)).toString(), barText); +// QCOMPARE(history->first()->next_uuid(), fooUuid); +// QCOMPARE(history->first()->previous_uuid(), fooUuid); +// QCOMPARE(history->find(fooUuid)->next_uuid(), barUuid); +// QCOMPARE(history->find(fooUuid)->previous_uuid(), barUuid); + + history->insert(QSharedPointer(new HistoryStringItem(fooBarText))); + QCOMPARE(history->data(history->index(0, 0)).toString(), fooBarText); +// QCOMPARE(history->first()->next_uuid(), barUuid); +// QCOMPARE(history->first()->previous_uuid(), fooUuid); +// QCOMPARE(history->find(fooUuid)->next_uuid(), foobarUuid); +// QCOMPARE(history->find(fooUuid)->previous_uuid(), barUuid); +// QCOMPARE(history->find(barUuid)->next_uuid(), fooUuid); +// QCOMPARE(history->find(barUuid)->previous_uuid(), foobarUuid); + + // insert one again - it should be moved to top + history->insert(QSharedPointer(new HistoryStringItem(barText))); + QCOMPARE(history->data(history->index(0, 0)).toString(), barText); +// QCOMPARE(history->first()->next_uuid(), foobarUuid); +// QCOMPARE(history->first()->previous_uuid(), fooUuid); +// QCOMPARE(history->find(fooUuid)->next_uuid(), barUuid); +// QCOMPARE(history->find(fooUuid)->previous_uuid(), foobarUuid); +// QCOMPARE(history->find(foobarUuid)->next_uuid(), fooUuid); +// QCOMPARE(history->find(foobarUuid)->previous_uuid(), barUuid); + + // move one to top using the slot + // already on top, shouldn't change anything + history->moveToTop(barUuid); + QCOMPARE(history->data(history->index(0, 0)).toString(), barText); +// QCOMPARE(history->first()->next_uuid(), foobarUuid); +// QCOMPARE(history->first()->previous_uuid(), fooUuid); +// QCOMPARE(history->find(fooUuid)->next_uuid(), barUuid); +// QCOMPARE(history->find(fooUuid)->previous_uuid(), foobarUuid); +// QCOMPARE(history->find(foobarUuid)->next_uuid(), fooUuid); +// QCOMPARE(history->find(foobarUuid)->previous_uuid(), barUuid); + + // another one should change, though + history->moveToTop(foobarUuid); + QCOMPARE(history->data(history->index(0, 0)).toString(), fooBarText); +// QCOMPARE(history->first()->next_uuid(), barUuid); +// QCOMPARE(history->first()->previous_uuid(), fooUuid); +// QCOMPARE(history->find(fooUuid)->next_uuid(), foobarUuid); +// QCOMPARE(history->find(fooUuid)->previous_uuid(), barUuid); +// QCOMPARE(history->find(barUuid)->next_uuid(), fooUuid); +// QCOMPARE(history->find(barUuid)->previous_uuid(), foobarUuid); + + // remove them again + QVERIFY(history->remove(foobarUuid)); + QCOMPARE(history->data(history->index(0, 0)).toString(), barText); +// QCOMPARE(history->first()->next_uuid(), fooUuid); +// QCOMPARE(history->first()->previous_uuid(), fooUuid); +// QCOMPARE(history->find(fooUuid)->next_uuid(), barUuid); +// QCOMPARE(history->find(fooUuid)->previous_uuid(), barUuid); + + QVERIFY(history->remove(barUuid)); + QCOMPARE(history->data(history->index(0, 0)).toString(), fooText); +// QCOMPARE(history->first()->next_uuid(), history->first()->uuid()); +// QCOMPARE(history->first()->previous_uuid(), history->first()->uuid()); + + QVERIFY(history->remove(fooUuid)); + QCOMPARE(history->rowCount(), 0); +} + +void HistoryModelTest::testClear() +{ + QScopedPointer history(new HistoryModel(nullptr)); + QScopedPointer modelTest(new ModelTest(history.data())); + history->setMaxSize(10); + QCOMPARE(history->rowCount(), 0); + + history->clear(); + QCOMPARE(history->rowCount(), 0); + + // insert some items + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foo")))); + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("bar")))); + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foobar")))); + history->moveToTop(QCryptographicHash::hash(QByteArrayLiteral("bar"), QCryptographicHash::Sha1)); + QCOMPARE(history->rowCount(), 3); + + // and clear + history->clear(); + QCOMPARE(history->rowCount(), 0); +} + +void HistoryModelTest::testIndexOf() +{ + QScopedPointer history(new HistoryModel(nullptr)); + QScopedPointer modelTest(new ModelTest(history.data())); + history->setMaxSize(10); + QCOMPARE(history->rowCount(), 0); + QVERIFY(!history->indexOf(QByteArrayLiteral("whatever")).isValid()); + QVERIFY(!history->indexOf(QByteArray()).isValid()); + + // insert some items + history->insert(QSharedPointer(new HistoryStringItem(QStringLiteral("foo")))); + QVERIFY(!history->indexOf(QByteArrayLiteral("whatever")).isValid()); + QVERIFY(!history->indexOf(QByteArray()).isValid()); + const QByteArray fooUuid = QCryptographicHash::hash(QByteArrayLiteral("foo"), QCryptographicHash::Sha1); + QVERIFY(history->indexOf(fooUuid).isValid()); + QCOMPARE(history->indexOf(fooUuid).data(Qt::UserRole+1).toByteArray(), fooUuid); + + history->clear(); + QVERIFY(!history->indexOf(fooUuid).isValid()); +} + +QTEST_MAIN(HistoryModelTest) +#include "historymodeltest.moc" diff --git a/klipper/autotests/modeltest.cpp b/klipper/autotests/modeltest.cpp new file mode 100644 index 000000000..d356b26c5 --- /dev/null +++ b/klipper/autotests/modeltest.cpp @@ -0,0 +1,600 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include + +#include "modeltest.h" + +#include + +/*! + Connect to all of the models signals. Whenever anything happens recheck everything. +*/ +ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false ) +{ + if (!model) + qFatal("%s: model must not be null", Q_FUNC_INFO); + + connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + + // Special checks for changes + connect(model, SIGNAL(layoutAboutToBeChanged()), + this, SLOT(layoutAboutToBeChanged()) ); + connect(model, SIGNAL(layoutChanged()), + this, SLOT(layoutChanged()) ); + + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex)) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(headerDataChanged(Qt::Orientation,int,int)) ); + + runAllTests(); +} + +void ModelTest::runAllTests() +{ + if ( fetchingMore ) + return; + nonDestructiveBasicTest(); + rowCount(); + columnCount(); + hasIndex(); + index(); + parent(); + data(); +} + +/*! + nonDestructiveBasicTest tries to call a number of the basic functions (not all) + to make sure the model doesn't outright segfault, testing the functions that makes sense. +*/ +void ModelTest::nonDestructiveBasicTest() +{ + QVERIFY( model->buddy ( QModelIndex() ) == QModelIndex() ); + model->canFetchMore ( QModelIndex() ); + QVERIFY( model->columnCount ( QModelIndex() ) >= 0 ); + QVERIFY( model->data ( QModelIndex() ) == QVariant() ); + fetchingMore = true; + model->fetchMore ( QModelIndex() ); + fetchingMore = false; + Qt::ItemFlags flags = model->flags ( QModelIndex() ); + QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 ); + model->hasChildren ( QModelIndex() ); + model->hasIndex ( 0, 0 ); + model->headerData ( 0, Qt::Horizontal ); + model->index ( 0, 0 ); + model->itemData ( QModelIndex() ); + QVariant cache; + model->match ( QModelIndex(), -1, cache ); + model->mimeTypes(); + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + QVERIFY( model->rowCount() >= 0 ); + QVariant variant; + model->setData ( QModelIndex(), variant, -1 ); + model->setHeaderData ( -1, Qt::Horizontal, QVariant() ); + model->setHeaderData ( 999999, Qt::Horizontal, QVariant() ); + QMap roles; + model->sibling ( 0, 0, QModelIndex() ); + model->span ( QModelIndex() ); + model->supportedDropActions(); +} + +/*! + Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() + + Models that are dynamically populated are not as fully tested here. + */ +void ModelTest::rowCount() +{ +// qDebug() << "rc"; + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + int rows = model->rowCount ( topIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( topIndex ) ); + + QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex ); + if ( secondLevelIndex.isValid() ) { // not the top level + // check a row count where parent is valid + rows = model->rowCount ( secondLevelIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( secondLevelIndex ) ); + } + + // The models rowCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() + */ +void ModelTest::columnCount() +{ + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->columnCount ( topIndex ) >= 0 ); + + // check a column count where parent is valid + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + if ( childIndex.isValid() ) + QVERIFY( model->columnCount ( childIndex ) >= 0 ); + + // columnCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::hasIndex() + */ +void ModelTest::hasIndex() +{ +// qDebug() << "hi"; + // Make sure that invalid values returns an invalid index + QVERIFY( !model->hasIndex ( -2, -2 ) ); + QVERIFY( !model->hasIndex ( -2, 0 ) ); + QVERIFY( !model->hasIndex ( 0, -2 ) ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + // check out of bounds + QVERIFY( !model->hasIndex ( rows, columns ) ); + QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) ); + + if ( rows > 0 ) + QVERIFY( model->hasIndex ( 0, 0 ) ); + + // hasIndex() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::index() + */ +void ModelTest::index() +{ +// qDebug() << "i"; + // Make sure that invalid values returns an invalid index + QVERIFY( model->index ( -2, -2 ) == QModelIndex() ); + QVERIFY( model->index ( -2, 0 ) == QModelIndex() ); + QVERIFY( model->index ( 0, -2 ) == QModelIndex() ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + if ( rows == 0 ) + return; + + // Catch off by one errors + QVERIFY( model->index ( rows, columns ) == QModelIndex() ); + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // Make sure that the same index is *always* returned + QModelIndex a = model->index ( 0, 0 ); + QModelIndex b = model->index ( 0, 0 ); + QVERIFY( a == b ); + + // index() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::parent() + */ +void ModelTest::parent() +{ +// qDebug() << "p"; + // Make sure the model won't crash and will return an invalid QModelIndex + // when asked for the parent of an invalid index. + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + + if ( model->rowCount() == 0 ) + return; + + // Column 0 | Column 1 | + // QModelIndex() | | + // \- topIndex | topIndex1 | + // \- childIndex | childIndex1 | + + // Common error test #1, make sure that a top level index has a parent + // that is a invalid QModelIndex. + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->parent ( topIndex ) == QModelIndex() ); + + // Common error test #2, make sure that a second level index has a parent + // that is the first level index. + if ( model->rowCount ( topIndex ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QVERIFY( model->parent ( childIndex ) == topIndex ); + } + + // Common error test #3, the second column should NOT have the same children + // as the first column in a row. + // Usually the second column shouldn't have children. + QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() ); + if ( model->rowCount ( topIndex1 ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 ); + QVERIFY( childIndex != childIndex1 ); + } + + // Full test, walk n levels deep through the model making sure that all + // parent's children correctly specify their parent. + checkChildren ( QModelIndex() ); +} + +/*! + Called from the parent() test. + + A model that returns an index of parent X should also return X when asking + for the parent of the index. + + This recursive function does pretty extensive testing on the whole model in an + effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() already work. + If they have a bug it will point it out, but the above tests should have already + found the basic bugs because it is easier to figure out the problem in + those tests then this one. + */ +void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) +{ + // First just try walking back up the tree. + QModelIndex p = parent; + while ( p.isValid() ) + p = p.parent(); + + // For models that are dynamically populated + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + + int rows = model->rowCount ( parent ); + int columns = model->columnCount ( parent ); + + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + // Some further testing against rows(), columns(), and hasChildren() + QVERIFY( rows >= 0 ); + QVERIFY( columns >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + //qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows + // << "columns:" << columns << "parent column:" << parent.column(); + + const QModelIndex topLeftChild = model->index( 0, 0, parent ); + + QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) ); + for ( int r = 0; r < rows; ++r ) { + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + QVERIFY( !model->hasIndex ( r, columns + 1, parent ) ); + for ( int c = 0; c < columns; ++c ) { + QVERIFY( model->hasIndex ( r, c, parent ) ); + QModelIndex index = model->index ( r, c, parent ); + // rowCount() and columnCount() said that it existed... + QVERIFY( index.isValid() ); + + // index() should always return the same index when called twice in a row + QModelIndex modifiedIndex = model->index ( r, c, parent ); + QVERIFY( index == modifiedIndex ); + + // Make sure we get the same index if we request it twice in a row + QModelIndex a = model->index ( r, c, parent ); + QModelIndex b = model->index ( r, c, parent ); + QVERIFY( a == b ); + + { + const QModelIndex sibling = model->sibling( r, c, topLeftChild ); + QVERIFY( index == sibling ); + } + { + const QModelIndex sibling = topLeftChild.sibling( r, c ); + QVERIFY( index == sibling ); + } + + // Some basic checking on the index that is returned + QVERIFY( index.model() == model ); + QCOMPARE( index.row(), r ); + QCOMPARE( index.column(), c ); + // While you can technically return a QVariant usually this is a sign + // of a bug in data(). Disable if this really is ok in your model. +// QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() ); + + // If the next test fails here is some somewhat useful debug you play with. + + if (model->parent(index) != parent) { + qDebug() << r << c << currentDepth << model->data(index).toString() + << model->data(parent).toString(); + qDebug() << index << parent << model->parent(index); +// And a view that you can even use to show the model. +// QTreeView view; +// view.setModel(model); +// view.show(); + } + + // Check that we can get back our real parent. + QCOMPARE( model->parent ( index ), parent ); + + // recursively go down the children + if ( model->hasChildren ( index ) && currentDepth < 10 ) { + //qDebug() << r << c << "has children" << model->rowCount(index); + checkChildren ( index, ++currentDepth ); + }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ + + // make sure that after testing the children that the index doesn't change. + QModelIndex newerIndex = model->index ( r, c, parent ); + QVERIFY( index == newerIndex ); + } + } +} + +/*! + Tests model's implementation of QAbstractItemModel::data() + */ +void ModelTest::data() +{ + // Invalid index should return an invalid qvariant + QVERIFY( !model->data ( QModelIndex() ).isValid() ); + + if ( model->rowCount() == 0 ) + return; + + // A valid index should have a valid QVariant data + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // shouldn't be able to set data on an invalid index + QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) ); + + // General Purpose roles that should return a QString + QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QSize + variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QFont + QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole ); + if ( fontVariant.isValid() ) { + QVERIFY( fontVariant.canConvert() ); + } + + // Check that the alignment is one we know about + QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole ); + if ( textAlignmentVariant.isValid() ) { + int alignment = textAlignmentVariant.toInt(); + QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) ); + } + + // General Purpose roles that should return a QColor + QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + // Check that the "check state" is one we know about. + QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole ); + if ( checkStateVariant.isValid() ) { + int state = checkStateVariant.toInt(); + QVERIFY( state == Qt::Unchecked || + state == Qt::PartiallyChecked || + state == Qt::Checked ); + } +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsInserted() + */ +void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */) +{ +// Q_UNUSED(end); +// qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString() +// << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) ); +// qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) ); + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( start, 0, parent ) ); + insert.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeInserted() + */ +void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end ) +{ + Changing c = insert.pop(); + QVERIFY( c.parent == parent ); +// qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize +// << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent ); + +// for (int ii=start; ii <= end; ii++) +// { +// qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent )); +// } +// qDebug(); + + QVERIFY( c.oldSize + ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + + if (c.next != model->data(model->index(end + 1, 0, c.parent))) { + qDebug() << start << end; + for (int i=0; i < model->rowCount(); ++i) + qDebug() << model->index(i, 0).data().toString(); + qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); + } + + QVERIFY( c.next == model->data ( model->index ( end + 1, 0, c.parent ) ) ); +} + +void ModelTest::layoutAboutToBeChanged() +{ + for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i ) + changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) ); +} + +void ModelTest::layoutChanged() +{ + for ( int i = 0; i < changing.count(); ++i ) { + QPersistentModelIndex p = changing[i]; + QVERIFY( p == model->index ( p.row(), p.column(), p.parent() ) ); + } + changing.clear(); +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsRemoved() + */ +void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end ) +{ +qDebug() << "ratbr" << parent << start << end; + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( end + 1, 0, parent ) ); + remove.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeRemoved() + */ +void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end ) +{ + qDebug() << "rr" << parent << start << end; + Changing c = remove.pop(); + QVERIFY( c.parent == parent ); + QVERIFY( c.oldSize - ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + QVERIFY( c.next == model->data ( model->index ( start, 0, c.parent ) ) ); +} + +void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QVERIFY(topLeft.isValid()); + QVERIFY(bottomRight.isValid()); + QModelIndex commonParent = bottomRight.parent(); + QVERIFY(topLeft.parent() == commonParent); + QVERIFY(topLeft.row() <= bottomRight.row()); + QVERIFY(topLeft.column() <= bottomRight.column()); + int rowCount = model->rowCount(commonParent); + int columnCount = model->columnCount(commonParent); + QVERIFY(bottomRight.row() < rowCount); + QVERIFY(bottomRight.column() < columnCount); +} + +void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end) +{ + QVERIFY(start >= 0); + QVERIFY(end >= 0); + QVERIFY(start <= end); + int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); + QVERIFY(start < itemCount); + QVERIFY(end < itemCount); +} + diff --git a/klipper/autotests/modeltest.h b/klipper/autotests/modeltest.h new file mode 100644 index 000000000..45c68f4b6 --- /dev/null +++ b/klipper/autotests/modeltest.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef MODELTEST_H +#define MODELTEST_H + +#include +#include +#include + +class ModelTest : public QObject +{ + Q_OBJECT + +public: + ModelTest( QAbstractItemModel *model, QObject *parent = 0 ); + +private Q_SLOTS: + void nonDestructiveBasicTest(); + void rowCount(); + void columnCount(); + void hasIndex(); + void index(); + void parent(); + void data(); + +protected Q_SLOTS: + void runAllTests(); + void layoutAboutToBeChanged(); + void layoutChanged(); + void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end ); + void rowsInserted( const QModelIndex & parent, int start, int end ); + void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end ); + void rowsRemoved( const QModelIndex & parent, int start, int end ); + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void headerDataChanged(Qt::Orientation orientation, int start, int end); + +private: + void checkChildren( const QModelIndex &parent, int currentDepth = 0 ); + + QAbstractItemModel *model; + + struct Changing { + QModelIndex parent; + int oldSize; + QVariant last; + QVariant next; + }; + QStack insert; + QStack remove; + + bool fetchingMore; + + QList changing; +}; + +#endif diff --git a/klipper/historymodel.cpp b/klipper/historymodel.cpp new file mode 100644 index 000000000..12c6fb7d4 --- /dev/null +++ b/klipper/historymodel.cpp @@ -0,0 +1,169 @@ +/******************************************************************** +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "historymodel.h" +#include "historyitem.h" + +#include + +Q_DECLARE_METATYPE(HistoryItem*) + +HistoryModel::HistoryModel(QObject *parent) + : QAbstractListModel(parent) + , m_maxSize(0) +{ +} + +HistoryModel::~HistoryModel() +{ + clear(); +} + +void HistoryModel::clear() +{ + beginResetModel(); + m_items.clear(); + endResetModel(); +} + +void HistoryModel::setMaxSize(int size) +{ + if (m_maxSize == size) { + return; + } + m_maxSize = size; + if (m_items.count() > m_maxSize) { + removeRows(m_maxSize, m_items.count() - m_maxSize); + } +} + +int HistoryModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_items.count(); +} + +QVariant HistoryModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_items.count() || index.column() != 0) { + return QVariant(); + } + + QSharedPointer item = m_items.at(index.row()); + switch (role) { + case Qt::DisplayRole: + return item->text(); + case Qt::UserRole: + return qVariantFromValue(item.data()); + case Qt::UserRole+1: + return item->uuid(); + } + return QVariant(); +} + +bool HistoryModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) { + return false; + } + if ((row + count) > m_items.count()) { + return false; + } + beginRemoveRows(QModelIndex(), row, row + count - 1); + for (int i = 0; i < count; ++i) { + m_items.removeAt(row); + } + endRemoveRows(); + return true; +} + +bool HistoryModel::remove(const QByteArray &uuid) +{ + QModelIndex index = indexOf(uuid); + if (!index.isValid()) { + return false; + } + return removeRow(index.row(), QModelIndex()); +} + +QModelIndex HistoryModel::indexOf(const QByteArray &uuid) const +{ + for (int i = 0; i < m_items.count(); ++i) { + if (m_items.at(i)->uuid() == uuid) { + return index(i); + } + } + return QModelIndex(); +} + +QModelIndex HistoryModel::indexOf(const HistoryItem *item) const +{ + if (!item) { + return QModelIndex(); + } + return indexOf(item->uuid()); +} + +void HistoryModel::insert(QSharedPointer item) +{ + if (item.isNull()) { + return; + } + const QModelIndex existingItem = indexOf(item.data()); + if (existingItem.isValid()) { + // move to top + moveToTop(existingItem.row()); + return; + } + + if (m_items.count() == m_maxSize) { + // remove last item + if (m_maxSize == 0) { + // special case - cannot insert any items + return; + } + beginRemoveRows(QModelIndex(), m_items.count() - 1, m_items.count() - 1); + m_items.removeLast(); + endRemoveRows(); + } + + beginInsertRows(QModelIndex(), 0, 0); + m_items.prepend(item); + endInsertRows(); +} + +void HistoryModel::moveToTop(const QByteArray &uuid) +{ + const QModelIndex existingItem = indexOf(uuid); + if (!existingItem.isValid()) { + return; + } + moveToTop(existingItem.row()); +} + +void HistoryModel::moveToTop(int row) +{ + if (row == 0 || row >= m_items.count()) { + return; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), 0); + m_items.move(row, 0); + endMoveRows(); +} diff --git a/klipper/historymodel.h b/klipper/historymodel.h new file mode 100644 index 000000000..b4fdc67ea --- /dev/null +++ b/klipper/historymodel.h @@ -0,0 +1,59 @@ +/******************************************************************** +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#ifndef KLIPPER_HISTORYMODEL_H +#define KLIPPER_HISTORYMODEL_H + +#include + +class HistoryItem; + +class HistoryModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit HistoryModel(QObject *parent = nullptr); + virtual ~HistoryModel(); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool remove(const QByteArray &uuid); + + int maxSize() const; + void setMaxSize(int size); + + void clear(); + void moveToTop(const QByteArray &uuid); + + QModelIndex indexOf(const QByteArray &uuid) const; + QModelIndex indexOf(const HistoryItem *item) const; + + void insert(QSharedPointer item); + +private: + void moveToTop(int row); + QList> m_items; + int m_maxSize; +}; + +inline int HistoryModel::maxSize() const +{ + return m_maxSize; +} + +#endif