diff options
Diffstat (limited to 'tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp')
-rw-r--r-- | tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp | 763 |
1 files changed, 698 insertions, 65 deletions
diff --git a/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp b/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp index f5fcc35084..236cd6212f 100644 --- a/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp +++ b/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** 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 The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QListWidget> @@ -37,14 +12,18 @@ #include <QTest> #include <QTimer> #include <QtMath> +#include <QProxyStyle> +#include <QVBoxLayout> +#include <QDialog> #include <QtTest/private/qtesthelpers_p.h> #include <QtWidgets/private/qlistview_p.h> +#include <QtWidgets/private/qapplication_p.h> using namespace QTestPrivate; #if defined(Q_OS_WIN) -# include <windows.h> +# include <qt_windows.h> # include <QDialog> # include <QGuiApplication> # include <QVBoxLayout> @@ -84,13 +63,19 @@ public: using QListView::setSelection; using QListView::setViewportMargins; using QListView::startDrag; - using QListView::viewOptions; + using QListView::initViewItemOption; QRegion getVisualRegionForSelection() const { return QListView::visualRegionForSelection(selectionModel()->selection()); } + void moveEvent(QMoveEvent *e) override + { + QListView::moveEvent(e); + m_gotValidResizeEvent = !e->pos().isNull(); + } friend class tst_QListView; + bool m_gotValidResizeEvent = false; }; class tst_QListView : public QObject @@ -109,6 +94,8 @@ private slots: void moveCursor(); void moveCursor2(); void moveCursor3(); + void moveCursor4(); + void moveCursor5(); void indexAt(); void clicked(); void singleSelectionRemoveRow(); @@ -164,10 +151,18 @@ private slots: void taskQTBUG_39902_mutualScrollBars(); void horizontalScrollingByVerticalWheelEvents(); void taskQTBUG_7232_AllowUserToControlSingleStep(); + void taskQTBUG_58749_adjustToContent(); void taskQTBUG_51086_skippingIndexesInSelectedIndexes(); void taskQTBUG_47694_indexOutOfBoundBatchLayout(); + void moveLastRow(); void itemAlignment(); + void internalDragDropMove_data(); void internalDragDropMove(); + void spacingWithWordWrap_data(); + void spacingWithWordWrap(); + void scrollOnRemove_data(); + void scrollOnRemove(); + void wordWrapNullIcon(); }; // Testing get/set functions @@ -433,7 +428,7 @@ void tst_QListView::cursorMove() } break; default: - QVERIFY(false); + QFAIL(qPrintable(QStringLiteral("Unexpected key: %1").arg(key))); } QCoreApplication::processEvents(); @@ -571,6 +566,76 @@ void tst_QListView::moveCursor3() QCOMPARE(view.selectionModel()->currentIndex(), model.index(0, 0)); } +void tst_QListView::moveCursor4() +{ + int indexCount = 100; + PublicListView listView; + QStandardItemModel model; + for (int i = 0; i < 100; i++) + { + QStandardItem* item = new QStandardItem(QString("item 0%0").arg(i)); + QFont font = item->font(); + font.setPixelSize(14); + item->setFont(font); + model.appendRow(item); + } + QFont font = model.item(0)->font(); + font.setPixelSize(50); + font.setBold(true); + model.item(0)->setFont(font); + listView.setModel(&model); + listView.setFixedSize(200, 200); + listView.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listView.show(); + listView.selectionModel()->setCurrentIndex(model.index(0, 0), QItemSelectionModel::SelectCurrent); + + QModelIndex idx = listView.moveCursor(PublicListView::MovePageDown, Qt::NoModifier); + + int actualIndex = 0; + int indexHeight = 0; + while (indexHeight <= listView.viewport()->height()) { + indexHeight += listView.visualRect(model.item(actualIndex)->index()).height(); + actualIndex++; + } + QTRY_COMPARE(idx, model.index(actualIndex - 2, 0)); + idx = listView.moveCursor(PublicListView::MoveUp, Qt::NoModifier); + QTRY_COMPARE(idx, model.index(0, 0)); + + listView.setCurrentIndex(model.index(indexCount - 2, 0)); + idx = listView.moveCursor(PublicListView::MovePageDown, Qt::NoModifier); + QTRY_COMPARE(idx, model.index(99, 0)); + + listView.setCurrentIndex(model.index(3, 0)); + actualIndex = 3; + indexHeight = 0; + while (indexHeight <= listView.viewport()->height()) { + indexHeight += listView.visualRect(model.item(actualIndex)->index()).height(); + actualIndex++; + } + idx = listView.moveCursor(PublicListView::MovePageDown, Qt::NoModifier); + QTRY_COMPARE(idx, model.index(actualIndex - 2, 0)); +} + +void tst_QListView::moveCursor5() +{ + PublicListView listView; + QStandardItemModel model; + QIcon icon(QPixmap(300,300)); + model.appendRow(new QStandardItem(icon,"11")); + model.appendRow(new QStandardItem(icon,"22")); + model.appendRow(new QStandardItem(icon,"33")); + listView.setModel(&model); + listView.setGeometry(10,10,200,200); + listView.setIconSize(QSize(300,300)); + listView.setViewMode(QListView::IconMode); + listView.setCurrentIndex(model.index(0, 0)); + + QModelIndex idx = listView.moveCursor(PublicListView::MovePageDown, Qt::NoModifier); + QTRY_COMPARE(idx, model.index(1, 0)); + idx = listView.moveCursor(PublicListView::MovePageUp, Qt::NoModifier); + QTRY_COMPARE(idx, model.index(0, 0)); +} class QListViewShowEventListener : public QListView { @@ -661,7 +726,7 @@ void tst_QListView::clicked() continue; QSignalSpy spy(&view, &QListView::clicked); QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, p); - QCOMPARE(spy.count(), 1); + QCOMPARE(spy.size(), 1); } } @@ -1090,7 +1155,7 @@ void tst_QListView::selection() v.setSelection(selectionRect, QItemSelectionModel::ClearAndSelect); const QModelIndexList selected = v.selectionModel()->selectedIndexes(); - QCOMPARE(selected.count(), expectedItems.count()); + QCOMPARE(selected.size(), expectedItems.size()); for (const auto &idx : selected) QVERIFY(expectedItems.contains(idx.row())); } @@ -1500,7 +1565,7 @@ void tst_QListView::task203585_selectAll() QVERIFY(view.selectionModel()->selectedIndexes().isEmpty()); view.setRowHidden(0, false); view.selectAll(); - QCOMPARE(view.selectionModel()->selectedIndexes().count(), 1); + QCOMPARE(view.selectionModel()->selectedIndexes().size(), 1); } void tst_QListView::task228566_infiniteRelayout() @@ -1585,7 +1650,7 @@ void tst_QListView::task196118_visualRegionForSelection() view.selectionModel()->select(top1.index(), QItemSelectionModel::Select); - QCOMPARE(view.selectionModel()->selectedIndexes().count(), 1); + QCOMPARE(view.selectionModel()->selectedIndexes().size(), 1); QVERIFY(view.getVisualRegionForSelection().isEmpty()); } @@ -1642,7 +1707,6 @@ void tst_QListView::keyboardSearch() QListView view; view.setModel(&model); view.show(); - QApplication::setActiveWindow(&view); QVERIFY(QTest::qWaitForWindowActive(&view)); QTest::keyClick(&view, Qt::Key_K); @@ -1683,7 +1747,7 @@ void tst_QListView::shiftSelectionWithNonUniformItemSizes() QTRY_COMPARE(view.currentIndex(), model.index(1, 0)); QModelIndexList selected = view.selectionModel()->selectedIndexes(); - QCOMPARE(selected.count(), 3); + QCOMPARE(selected.size(), 3); QVERIFY(!selected.contains(model.index(0, 0))); } { // Second test: QListView::TopToBottom flow @@ -1710,7 +1774,7 @@ void tst_QListView::shiftSelectionWithNonUniformItemSizes() QTRY_COMPARE(view.currentIndex(), model.index(1, 0)); QModelIndexList selected = view.selectionModel()->selectedIndexes(); - QCOMPARE(selected.count(), 3); + QCOMPARE(selected.size(), 3); QVERIFY(!selected.contains(model.index(0, 0))); } } @@ -1744,7 +1808,6 @@ void tst_QListView::shiftSelectionWithItemAlignment() view.resize(300, view.sizeHintForRow(0) * items.size() / 2 + view.horizontalScrollBar()->height()); view.show(); - QApplication::setActiveWindow(&view); QVERIFY(QTest::qWaitForWindowActive(&view)); QCOMPARE(static_cast<QWidget *>(&view), QApplication::activeWindow()); @@ -1772,7 +1835,7 @@ void tst_QListView::clickOnViewportClearsSelection() view.selectAll(); QModelIndex index = model.index(0); - QCOMPARE(view.selectionModel()->selectedIndexes().count(), 1); + QCOMPARE(view.selectionModel()->selectedIndexes().size(), 1); QVERIFY(view.selectionModel()->isSelected(index)); //we try to click outside of the index @@ -1780,7 +1843,7 @@ void tst_QListView::clickOnViewportClearsSelection() QTest::mousePress(view.viewport(), Qt::LeftButton, {}, point); //at this point, the selection shouldn't have changed - QCOMPARE(view.selectionModel()->selectedIndexes().count(), 1); + QCOMPARE(view.selectionModel()->selectedIndexes().size(), 1); QVERIFY(view.selectionModel()->isSelected(index)); QTest::mouseRelease(view.viewport(), Qt::LeftButton, {}, point); @@ -1803,7 +1866,6 @@ void tst_QListView::task262152_setModelColumnNavigate() view.setModelColumn(1); view.show(); - QApplication::setActiveWindow(&view); QVERIFY(QTest::qWaitForWindowActive(&view)); QCOMPARE(&view, QApplication::activeWindow()); QTest::keyClick(&view, Qt::Key_Down); @@ -1889,7 +1951,7 @@ void tst_QListView::taskQTBUG_435_deselectOnViewportClick() view.setModel(&model); view.setSelectionMode(QAbstractItemView::ExtendedSelection); view.selectAll(); - QCOMPARE(view.selectionModel()->selectedIndexes().count(), model.rowCount()); + QCOMPARE(view.selectionModel()->selectedIndexes().size(), model.rowCount()); const QRect itemRect = view.visualRect(model.index(model.rowCount() - 1)); @@ -1899,7 +1961,7 @@ void tst_QListView::taskQTBUG_435_deselectOnViewportClick() QVERIFY(!view.selectionModel()->hasSelection()); view.selectAll(); - QCOMPARE(view.selectionModel()->selectedIndexes().count(), model.rowCount()); + QCOMPARE(view.selectionModel()->selectedIndexes().size(), model.rowCount()); //and now the right button QTest::mouseClick(view.viewport(), Qt::RightButton, {}, p); @@ -2258,10 +2320,11 @@ void tst_QListView::testScrollToWithHidden() void tst_QListView::testViewOptions() { PublicListView view; - QStyleOptionViewItem options = view.viewOptions(); + QStyleOptionViewItem options; + view.initViewItemOption(&options); QCOMPARE(options.decorationPosition, QStyleOptionViewItem::Left); view.setViewMode(QListView::IconMode); - options = view.viewOptions(); + view.initViewItemOption(&options); QCOMPARE(options.decorationPosition, QStyleOptionViewItem::Top); } @@ -2469,6 +2532,46 @@ void tst_QListView::taskQTBUG_7232_AllowUserToControlSingleStep() QCOMPARE(hStep1, lv.horizontalScrollBar()->singleStep()); } +void tst_QListView::taskQTBUG_58749_adjustToContent() +{ + QStandardItemModel model; + model.setRowCount(20); + model.setColumnCount(1); + const QString rowStr = QStringLiteral("Row number txt:"); + for (int u = 0; u < model.rowCount(); ++u) + model.setData(model.index(u, 0), rowStr + QString::number(u)); + + QDialog w; // It really should work for QWidget, too, but sometimes an event (like move) + // is needed to get the resize triggered. + QVBoxLayout *l = new QVBoxLayout(&w); + l->setSizeConstraint(QLayout::SetFixedSize); + auto *view = new QListView; + view->setModel(&model); + view->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + l->addWidget(view); + l->setSizeConstraint(QLayout::SetFixedSize); + w.show(); + QVERIFY(QTest::qWaitForWindowExposed(&w)); + + const QString longText = "Here we have a row text that is somewhat longer ..."; + + QFontMetrics fm(w.font(), &w); + QRect r = fm.boundingRect(model.data(model.index(0, 0)).toString()); + const int longTextWidth = fm.horizontalAdvance(longText); + QVERIFY(w.height() > r.height() * model.rowCount()); + // We have a width longer than the width for the given index data. + QVERIFY(w.width() > r.width()); + // but ... the width should not have a width matching the much longer text. + QVERIFY(w.width() < longTextWidth); + + // use the long text and make sure the width is adjusted. + model.setData(model.index(0, 0), longText); + QApplication::processEvents(); + const QRect itemRect = view->visualRect(model.index(0, 0)); + QVERIFY(w.width() > itemRect.width()); + QCOMPARE_GE(view->width(), itemRect.width()); +} + void tst_QListView::taskQTBUG_51086_skippingIndexesInSelectedIndexes() { QStandardItemModel data(10, 1); @@ -2502,6 +2605,221 @@ void tst_QListView::taskQTBUG_47694_indexOutOfBoundBatchLayout() view.scrollTo(model.index(batchSize - 1, 0)); } +class TstMoveItem +{ + friend class TstMoveModel; +public: + TstMoveItem(TstMoveItem *parent = nullptr) + : parentItem(parent) + { + if (parentItem) + parentItem->childItems.append(this); + } + + ~TstMoveItem() + { + QList<TstMoveItem *> delItms; + delItms.swap(childItems); + qDeleteAll(delItms); + + if (parentItem) + parentItem->childItems.removeAll(this); + } + + int row() + { + if (parentItem) + return parentItem->childItems.indexOf(this); + return -1; + } + +public: + TstMoveItem *parentItem = nullptr; + QList<TstMoveItem *> childItems; + QHash<int, QVariant> data; +}; + +/*! + Test that removing the last row in an IconView mode QListView + doesn't crash. The model is specifically crafted to provoke a + stale QBspTree by returning a 0 column count for indexes without + children, which changes the column count after moving the last row. + + See QTBUG_95463. +*/ +class TstMoveModel : public QAbstractItemModel +{ + Q_OBJECT +public: + TstMoveModel(QObject *parent = nullptr) + : QAbstractItemModel(parent) + { + rootItem = new TstMoveItem; + rootItem->data.insert(Qt::DisplayRole, "root"); + + TstMoveItem *itm = new TstMoveItem(rootItem); + itm->data.insert(Qt::DisplayRole, "parentItem1"); + + TstMoveItem *itmCh = new TstMoveItem(itm); + itmCh->data.insert(Qt::DisplayRole, "childItem"); + + itm = new TstMoveItem(rootItem); + itm->data.insert(Qt::DisplayRole, "parentItem2"); + } + + ~TstMoveModel() + { + delete rootItem; + } + + QModelIndex index(int row, int column, const QModelIndex &idxPar = QModelIndex()) const override + { + QModelIndex idx; + if (hasIndex(row, column, idxPar)) { + TstMoveItem *parentItem = nullptr; + if (idxPar.isValid()) + parentItem = static_cast<TstMoveItem *>(idxPar.internalPointer()); + else + parentItem = rootItem; + + Q_ASSERT(parentItem); + TstMoveItem *childItem = parentItem->childItems.at(row); + if (childItem) + idx = createIndex(row, column, childItem); + } + return idx; + } + + QModelIndex parent(const QModelIndex &index) const override + { + QModelIndex idxPar; + if (index.isValid()) { + TstMoveItem *childItem = static_cast<TstMoveItem *>(index.internalPointer()); + TstMoveItem *parentItem = childItem->parentItem; + if (parentItem != rootItem) + idxPar = createIndex(parentItem->row(), 0, parentItem); + } + return idxPar; + } + + int columnCount(const QModelIndex &idxPar = QModelIndex()) const override + { + int cnt = 0; + if (idxPar.isValid()) { + TstMoveItem *parentItem = static_cast<TstMoveItem *>(idxPar.internalPointer()); + Q_ASSERT(parentItem); + cnt = parentItem->childItems.isEmpty() ? 0 : 1; + } else { + cnt = rootItem->childItems.isEmpty() ? 0 : 1; + } + return cnt; + } + + int rowCount(const QModelIndex &idxPar = QModelIndex()) const override + { + int cnt = 0; + if (idxPar.isValid()) { + TstMoveItem *parentItem = static_cast<TstMoveItem *>(idxPar.internalPointer()); + Q_ASSERT(parentItem); + cnt = parentItem->childItems.size(); + } else { + cnt = rootItem->childItems.size(); + } + return cnt; + } + + Qt::ItemFlags flags(const QModelIndex &index) const override + { + Q_UNUSED(index) + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + } + + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override + { + bool ret = false; + if (parent.isValid()) { + TstMoveItem *parentItem = static_cast<TstMoveItem *>(parent.internalPointer()); + Q_ASSERT(parentItem); + ret = parentItem->childItems.size() > 0; + } else { + ret = rootItem->childItems.size() > 0; + } + return ret; + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + QVariant dt; + if (index.isValid()) { + TstMoveItem *item = static_cast<TstMoveItem *>(index.internalPointer()); + if (item) + dt = item->data.value(role); + } + return dt; + } + + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) override + { + TstMoveItem *itmSrcParent = itemAt(sourceParent); + TstMoveItem *itmDestParent = itemAt(destinationParent); + + if (itmSrcParent && sourceRow >= 0 + && sourceRow + count <= itmSrcParent->childItems.size() + && itmDestParent && destinationChild <= itmDestParent->childItems.size()) { + beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, + destinationParent, destinationChild); + QList<TstMoveItem *> itemsToMove; + for (int i = 0; i < count; ++i) { + TstMoveItem *itm = itmSrcParent->childItems.at(sourceRow+i); + itemsToMove.append(itm); + } + for (int i = itemsToMove.size() -1; i >= 0; --i) { + TstMoveItem *itm = itemsToMove.at(i); + itm->parentItem->childItems.removeAll(itm); + itm->parentItem = itmDestParent; + itmDestParent->childItems.insert(destinationChild, itm); + } + endMoveRows(); + return true; + } + return false; + } + +private: + TstMoveItem *itemAt(const QModelIndex &index) const + { + TstMoveItem *item = nullptr; + if (index.isValid()) { + Q_ASSERT(index.model() == this); + item = static_cast<TstMoveItem *>(index.internalPointer()); + } else { + item = rootItem; + } + return item; + } + +private: + TstMoveItem *rootItem = nullptr; +}; + +void tst_QListView::moveLastRow() +{ + TstMoveModel model; + QListView view; + view.setModel(&model); + view.setRootIndex(model.index(0, 0, QModelIndex())); + view.setViewMode(QListView::IconMode); + view.show(); + + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QModelIndex sourceParent = model.index(0, 0); + QModelIndex destinationParent = model.index(1, 0); + // must not crash when paint event is processed + model.moveRow(sourceParent, 0, destinationParent, 0); + QTest::qWait(100); +} + void tst_QListView::itemAlignment() { auto item1 = new QStandardItem("111"); @@ -2525,46 +2843,361 @@ void tst_QListView::itemAlignment() QVERIFY(w.visualRect(item1->index()).width() < w.visualRect(item2->index()).width()); } +void tst_QListView::internalDragDropMove_data() +{ + QTest::addColumn<QListView::ViewMode>("viewMode"); + QTest::addColumn<QAbstractItemView::DragDropMode>("dragDropMode"); + QTest::addColumn<Qt::DropActions>("supportedDropActions"); + QTest::addColumn<Qt::DropAction>("defaultDropAction"); + QTest::addColumn<Qt::ItemFlags>("itemFlags"); + QTest::addColumn<bool>("modelMoves"); + QTest::addColumn<QStringList>("expectedData"); + + const Qt::ItemFlags defaultFlags = Qt::ItemIsSelectable + | Qt::ItemIsEnabled + | Qt::ItemIsEditable + | Qt::ItemIsDragEnabled; + + const QStringList unchanged = QStringList{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; + const QStringList reordered = QStringList{"0", "2", "3", "4", "5", "6", "7", "8", "9", "1"}; + const QStringList replaced = QStringList{"0", "2", "3", "4", "1", "6", "7", "8", "9"}; + + for (auto viewMode : { QListView::IconMode, QListView::ListMode }) { + for (auto modelMoves : { true, false } ) { + QByteArray rowName = viewMode == QListView::IconMode ? "icon" : "list" ; + rowName += modelMoves ? ", model moves" : ", model doesn't move"; + QTest::newRow((rowName + ", copy&move").constData()) + << viewMode + << QAbstractItemView::InternalMove + << (Qt::CopyAction|Qt::MoveAction) + << Qt::MoveAction + << defaultFlags + << modelMoves + // listview in IconMode doesn't change the model + << ((viewMode == QListView::IconMode && !modelMoves) ? unchanged : reordered); + + QTest::newRow((rowName + ", only move").constData()) + << viewMode + << QAbstractItemView::InternalMove + << (Qt::IgnoreAction|Qt::MoveAction) + << Qt::MoveAction + << defaultFlags + << modelMoves + // listview in IconMode doesn't change the model + << ((viewMode == QListView::IconMode && !modelMoves) ? unchanged : reordered); + + QTest::newRow((rowName + ", replace item").constData()) + << viewMode + << QAbstractItemView::InternalMove + << (Qt::IgnoreAction|Qt::MoveAction) + << Qt::MoveAction + << (defaultFlags | Qt::ItemIsDropEnabled) + << modelMoves + << replaced; + } + } +} + +/* + Test moving of items items via drag'n'drop. + + This should reorder items when an item is dropped in between two items, + or - if items can be dropped on - replace the content of the drop target. + + Test QListView in both icon and list view modes. + + See QTBUG-67440, QTBUG-83084, QTBUG-87057 +*/ void tst_QListView::internalDragDropMove() { const QString platform(QGuiApplication::platformName().toLower()); if (platform != QLatin1String("xcb")) QSKIP("Need a window system with proper DnD support via injected mouse events"); - // on an internal move, the item was deleted which should not happen - // see QTBUG-67440 + QFETCH(QListView::ViewMode, viewMode); + QFETCH(QAbstractItemView::DragDropMode, dragDropMode); + QFETCH(Qt::DropActions, supportedDropActions); + QFETCH(Qt::DropAction, defaultDropAction); + QFETCH(Qt::ItemFlags, itemFlags); + QFETCH(bool, modelMoves); + QFETCH(QStringList, expectedData); + + class ItemModel : public QStringListModel + { + public: + ItemModel() + { + QStringList list; + for (int i = 0; i < 10; ++i) { + list << QString::number(i); + } + setStringList(list); + } + + Qt::DropActions supportedDropActions() const override { return m_supportedDropActions; } + Qt::ItemFlags flags(const QModelIndex &index) const override + { + if (!index.isValid()) + return QStringListModel::flags(index); + return m_itemFlags; + } + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, + const QModelIndex &destinationParent, int destinationChild) override + { + if (!m_modelMoves) // many models don't implement moveRows + return false; + return QStringListModel::moveRows(sourceParent, sourceRow, count, + destinationParent, destinationChild); + } + bool setItemData(const QModelIndex &index, const QMap<int, QVariant> &values) override + { + return QStringListModel::setData(index, values.value(Qt::DisplayRole), Qt::DisplayRole); + } + QVariant data(const QModelIndex &index, int role) const override + { + if (role == Qt::DecorationRole) + return QColor(Qt::GlobalColor(index.row() + 1)); + return QStringListModel::data(index, role); + } + QMap<int, QVariant> itemData(const QModelIndex &index) const override + { + auto item = QStringListModel::itemData(index); + item[Qt::DecorationRole] = data(index, Qt::DecorationRole); + return item; + } + + Qt::DropActions m_supportedDropActions; + Qt::ItemFlags m_itemFlags; + bool m_modelMoves; + }; + + ItemModel data; + data.m_supportedDropActions = supportedDropActions; + data.m_itemFlags = itemFlags; + data.m_modelMoves = modelMoves; - QStandardItemModel data(0, 1); - QPixmap pixmap(32, 32); - for (int i = 0; i < 10; ++i) { - pixmap.fill(Qt::GlobalColor(i + 1)); - data.appendRow(new QStandardItem(QIcon(pixmap), QString::number(i))); - } QItemSelectionModel selections(&data); PublicListView list; list.setWindowTitle(QTest::currentTestFunction()); - list.setViewMode(QListView::IconMode); - list.setDefaultDropAction(Qt::MoveAction); + list.setViewMode(viewMode); + list.setDragDropMode(dragDropMode); + list.setDefaultDropAction(defaultDropAction); list.setModel(&data); list.setSelectionModel(&selections); - list.resize(300, 300); + int itemHeight = list.sizeHintForIndex(data.index(1, 0)).height(); + list.resize(300, 15 * itemHeight); list.show(); selections.select(data.index(1, 0), QItemSelectionModel::Select); - QVERIFY(QTest::qWaitForWindowExposed(&list)); - + auto getSelectedTexts = [&]() -> QStringList { + QStringList selectedTexts; + for (auto index : selections.selectedIndexes()) + selectedTexts << data.itemData(index).value(Qt::DisplayRole).toString(); + return selectedTexts; + }; + // The test relies on the global position of mouse events; make sure + // the window is properly mapped on X11. + QVERIFY(QTest::qWaitForWindowActive(&list)); + QVERIFY(QTest::qWaitFor([&]() { return list.m_gotValidResizeEvent; })); // execute as soon as the eventloop is running again // which is the case inside list.startDrag() - QTimer::singleShot(0, [&list]() + QTimer::singleShot(0, [&]() { - const QPoint pos = list.rect().center(); - QMouseEvent mouseMove(QEvent::MouseMove, pos, list.mapToGlobal(pos), Qt::NoButton, {}, {}); + QPoint droppos; + // take into account subtle differences between icon and list mode in QListView's drop placement + if (itemFlags & Qt::ItemIsDropEnabled) + droppos = list.rectForIndex(data.index(5, 0)).center(); + else if (viewMode == QListView::IconMode) + droppos = list.rectForIndex(data.index(9, 0)).bottomRight() + QPoint(30, 30); + else + droppos = list.rectForIndex(data.index(9, 0)).bottomRight(); + + QMouseEvent mouseMove(QEvent::MouseMove, droppos, list.mapToGlobal(droppos), Qt::NoButton, {}, {}); QCoreApplication::sendEvent(&list, &mouseMove); - QMouseEvent mouseRelease(QEvent::MouseButtonRelease, pos, list.mapToGlobal(pos), Qt::LeftButton, {}, {}); + QMouseEvent mouseRelease(QEvent::MouseButtonRelease, droppos, list.mapToGlobal(droppos), Qt::LeftButton, {}, {}); QCoreApplication::sendEvent(&list, &mouseRelease); }); - const int expectedCount = data.rowCount(); - list.startDrag(Qt::MoveAction|Qt::CopyAction); - QCOMPARE(expectedCount, data.rowCount()); + + const QStringList expectedSelected = getSelectedTexts(); + + list.startDrag(Qt::MoveAction); + + QTRY_COMPARE(data.stringList(), expectedData); + + // if the model doesn't implement moveRows, or if items are replaced, then selection is lost + if (modelMoves && !(itemFlags & Qt::ItemIsDropEnabled)) { + const QStringList actualSelected = getSelectedTexts(); + QTRY_COMPARE(actualSelected, expectedSelected); + } +} + +/*! + Verify fix for QTBUG-92366 +*/ +void tst_QListView::spacingWithWordWrap_data() +{ + QTest::addColumn<bool>("scrollBarOverlap"); + + QTest::addRow("Without overlap") << false; + QTest::addRow("With overlap") << true; +} + +void tst_QListView::spacingWithWordWrap() +{ + QFETCH(bool, scrollBarOverlap); + + class MyStyle : public QProxyStyle + { + bool scrollBarOverlap; + public: + MyStyle(bool scrollBarOverlap) : scrollBarOverlap(scrollBarOverlap) {} + + int pixelMetric(PixelMetric metric, const QStyleOption *option = nullptr, + const QWidget *widget = nullptr) const override{ + switch (metric) { + case QStyle::PM_ScrollView_ScrollBarOverlap: return scrollBarOverlap; + default: + break; + } + return QProxyStyle::pixelMetric(metric, option, widget); + } + }; + + QStyle *oldStyle = QApplication::style(); + oldStyle->setParent(nullptr); + const auto resetStyle = qScopeGuard([oldStyle]{ + QApplication::setStyle(oldStyle); + }); + QApplication::setStyle(new MyStyle(scrollBarOverlap)); + + const int listViewResizeCount = 200; + QWidget window; + window.resize(300, 200); + QListView lv(&window); + + lv.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + lv.setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + lv.setFlow(QListView::TopToBottom); + lv.setWordWrap(true); + lv.setSpacing(0); + lv.setGeometry(0, 0, 200, 150); + + QStandardItem *it1 = new QStandardItem("qqqqqqqqqqqqqqqqqqqqq-ttttttttttttttttt"); + QStandardItem *it2 = new QStandardItem("qqqqqqqqqqqqqqqq-tttttttttttt"); + QStandardItemModel model; + lv.setModel(&model); + model.appendRow(it1); + model.appendRow(it2); + + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + QVERIFY(!lv.verticalScrollBar()->isVisible()); + for (int i = 0; i < listViewResizeCount; ++i) { + lv.resize(lv.width() + 1, lv.height()); + QRect rectForRowOne = lv.visualRect(model.index(0, 0)); + QRect rectForRowTwo = lv.visualRect(model.index(1, 0)); + + QCOMPARE(rectForRowOne.y() + rectForRowOne.height(), rectForRowTwo.y()); + } + + lv.resize(200, 150); + const QStringList &stringList = generateList(QStringLiteral("Test_Abnormal_Spacing"), 30); + for (const QString &item_string : stringList) { + QStandardItem *item = new QStandardItem(item_string); + model.appendRow(item); + } + + // test whether the height of item is correct if the vbar is shown. + QTRY_VERIFY(lv.verticalScrollBar()->isVisible()); + for (int i = 0; i < listViewResizeCount; ++i) { + lv.resize(lv.width() + 1, lv.height()); + QRect rectForRowOne = lv.visualRect(model.index(0, 0)); + QRect rectForRowTwo = lv.visualRect(model.index(1, 0)); + + QCOMPARE(rectForRowOne.y() + rectForRowOne.height(), rectForRowTwo.y()); + } +} + +void tst_QListView::scrollOnRemove_data() +{ + QTest::addColumn<QListView::ViewMode>("viewMode"); + QTest::addColumn<QAbstractItemView::SelectionMode>("selectionMode"); + + const QMetaObject &mo = QListView::staticMetaObject; + const auto viewModeEnum = mo.enumerator(mo.indexOfEnumerator("ViewMode")); + const auto selectionModeEnum = mo.enumerator(mo.indexOfEnumerator("SelectionMode")); + for (auto viewMode : { QListView::ListMode, QListView::IconMode }) { + const char *viewModeName = viewModeEnum.valueToKey(viewMode); + for (int index = 0; index < selectionModeEnum.keyCount(); ++index) { + const auto selectionMode = QAbstractItemView::SelectionMode(selectionModeEnum.value(index)); + const char *selectionModeName = selectionModeEnum.valueToKey(selectionMode); + QTest::addRow("%s, %s", viewModeName, selectionModeName) << viewMode << selectionMode; + } + } +} + +void tst_QListView::scrollOnRemove() +{ + QFETCH(QListView::ViewMode, viewMode); + QFETCH(QAbstractItemView::SelectionMode, selectionMode); + + QPixmap pixmap; + if (viewMode == QListView::IconMode) { + pixmap = QPixmap(25, 25); + pixmap.fill(Qt::red); + } + + QStandardItemModel model; + for (int i = 0; i < 50; ++i) { + QStandardItem *item = new QStandardItem(QString::number(i)); + item->setIcon(pixmap); + model.appendRow(item); + } + + QWidget widget; + QListView view(&widget); + view.setFixedSize(100, 100); + view.setAutoScroll(true); + if (viewMode == QListView::IconMode) + view.setWrapping(true); + view.setModel(&model); + view.setSelectionMode(selectionMode); + view.setViewMode(viewMode); + + widget.show(); + QVERIFY(QTest::qWaitForWindowExposed(&widget)); + + QCOMPARE(view.verticalScrollBar()->value(), 0); + const QModelIndex item25 = model.index(25, 0); + view.scrollTo(item25); + QTRY_VERIFY(view.verticalScrollBar()->value() > 0); // layout and scrolling are delayed + const int item25Position = view.verticalScrollBar()->value(); + // selecting a fully visible item shouldn't scroll + view.selectionModel()->setCurrentIndex(item25, QItemSelectionModel::SelectCurrent); + QTRY_COMPARE(view.verticalScrollBar()->value(), item25Position); + + // removing the selected item might scroll if another item is selected + model.removeRow(25); + + // if nothing is selected now, then the view should not have scrolled + if (!view.selectionModel()->selectedIndexes().size()) + QTRY_COMPARE(view.verticalScrollBar()->value(), item25Position); +} + +void tst_QListView::wordWrapNullIcon() +{ + QListView listView; + listView.setViewMode(QListView::IconMode); + listView.setWrapping(true); + listView.setWordWrap(true); + listView.setFixedSize(QSize(100, 500)); + + QStandardItemModel model; + QStandardItem *item = new QStandardItem(QIcon(), "This is a long text for word wrapping Item_"); + model.appendRow(item); + listView.setModel(&model); + + listView.indexAt(QPoint(0, 0)); } |