diff options
Diffstat (limited to 'tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp')
-rw-r--r-- | tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp | 471 |
1 files changed, 451 insertions, 20 deletions
diff --git a/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp b/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp index 8c1cff79ec..b038245610 100644 --- a/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp +++ b/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp @@ -166,8 +166,12 @@ private slots: void taskQTBUG_7232_AllowUserToControlSingleStep(); void taskQTBUG_51086_skippingIndexesInSelectedIndexes(); void taskQTBUG_47694_indexOutOfBoundBatchLayout(); + void moveLastRow(); void itemAlignment(); + void internalDragDropMove_data(); void internalDragDropMove(); + void scrollOnRemove_data(); + void scrollOnRemove(); }; // Testing get/set functions @@ -2511,6 +2515,222 @@ 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.count(); + } else { + cnt = rootItem->childItems.count(); + } + 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.count() > 0; + } else { + ret = rootItem->childItems.count() > 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.count() + && itmDestParent && destinationChild <= itmDestParent->childItems.count()) { + 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.count() -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(); + + QApplication::setActiveWindow(&view); + 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"); @@ -2534,46 +2754,257 @@ 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)); // 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); + } +} + + +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().count()) + QTRY_COMPARE(view.verticalScrollBar()->value(), item25Position); } |