summaryrefslogtreecommitdiffstats
path: root/tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp')
-rw-r--r--tests/auto/widgets/itemviews/qlistview/tst_qlistview.cpp471
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);
}