From 71aaf831d175a164b508ce169131a794d55d6fb0 Mon Sep 17 00:00:00 2001 From: Volker Hilsheimer Date: Thu, 21 Oct 2021 12:03:57 +0200 Subject: [PATCH] QAbstractItemView: auto-scroll with selection rectangle Some item views, such as QListView in icon mode, implement a selection rectangle with which the user can "lasso" items. So far, dragging that rectangle did not trigger auto scroll, so unless an item near the edge was selected, the user had to stop the lassoing and scroll manually to reach more items. Since QAbtractItemView implements auto scrolling for drag'n'drop, we can use that mechanism also when the selection rectangle is dragged. This requires some modifications: We need to make sure that scrolling the view during a drag-selection generates mouse move events so that the selection is extended and the rectangle is updated in subclasses. And we need to stop using QCursor::pos to get the position of the mouse pointer, as this makes the auto-scrolling untestable. Instead, record the mouse position last seen during a mouseMove or dragMoveEvent in content-coordinates (identical to pressedPosition). As a drive-by, fix some coding-style issues in nearby code. Done-with: Zhang Hao Fixes: QTBUG-96124 Change-Id: I426f786e5842ae9f9fb04e9d34dc6d3379a6207f Reviewed-by: Richard Moe Gustavsen --- src/widgets/itemviews/qabstractitemview.cpp | 29 +++-- src/widgets/itemviews/qabstractitemview_p.h | 1 + .../itemviews/qabstractitemview/qtlogo.png | Bin 0 -> 2991 bytes .../tst_qabstractitemview.cpp | 117 ++++++++++++++++++ 4 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 tests/auto/widgets/itemviews/qabstractitemview/qtlogo.png diff --git a/src/widgets/itemviews/qabstractitemview.cpp b/src/widgets/itemviews/qabstractitemview.cpp index d782eb71ab..3fb77b72a6 100644 --- a/src/widgets/itemviews/qabstractitemview.cpp +++ b/src/widgets/itemviews/qabstractitemview.cpp @@ -1798,8 +1798,7 @@ void QAbstractItemView::mousePressEvent(QMouseEvent *event) // this is the mouse press event that closed the last editor (via focus event) d->pressClosedEditor = d->pressClosedEditorWatcher.isActive() && d->lastEditedIndex == index; - if (!d->selectionModel - || (d->state == EditingState && d->hasEditor(index))) + if (!d->selectionModel || (d->state == EditingState && d->hasEditor(index))) return; d->pressedAlreadySelected = d->selectionModel->isSelected(index); @@ -1808,10 +1807,9 @@ void QAbstractItemView::mousePressEvent(QMouseEvent *event) QItemSelectionModel::SelectionFlags command = selectionCommand(index, event); d->noSelectionOnMousePress = command == QItemSelectionModel::NoUpdate || !index.isValid(); QPoint offset = d->offset(); - d->pressedPosition = pos + offset; - if ((command & QItemSelectionModel::Current) == 0) { + d->pressedPosition = d->draggedPosition = pos + offset; + if (!(command & QItemSelectionModel::Current)) d->currentSelectionStartIndex = index; - } else if (!d->currentSelectionStartIndex.isValid()) d->currentSelectionStartIndex = currentIndex(); @@ -1863,6 +1861,8 @@ void QAbstractItemView::mouseMoveEvent(QMouseEvent *event) QPoint topLeft; QPoint bottomRight = event->position().toPoint(); + d->draggedPosition = bottomRight + d->offset(); + if (state() == ExpandingState || state() == CollapsingState) return; @@ -1922,10 +1922,10 @@ void QAbstractItemView::mouseMoveEvent(QMouseEvent *event) setSelection(selectionRect, command); // set at the end because it might scroll the view - if (index.isValid() - && (index != d->selectionModel->currentIndex()) - && d->isIndexEnabled(index)) + if (index.isValid() && (index != d->selectionModel->currentIndex()) && d->isIndexEnabled(index)) d->selectionModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + else if (d->shouldAutoScroll(event->pos()) && !d->autoScrollTimer.isActive()) + startAutoScroll(); } } @@ -2047,6 +2047,7 @@ void QAbstractItemView::dragEnterEvent(QDragEnterEvent *event) void QAbstractItemView::dragMoveEvent(QDragMoveEvent *event) { Q_D(QAbstractItemView); + d->draggedPosition = event->position().toPoint() + d->offset(); if (dragDropMode() == InternalMove && (event->source() != this || !(event->possibleActions() & Qt::MoveAction))) return; @@ -3989,8 +3990,8 @@ void QAbstractItemView::doAutoScroll() int verticalValue = verticalScroll->value(); int horizontalValue = horizontalScroll->value(); - QPoint pos = d->viewport->mapFromGlobal(QCursor::pos()); - QRect area = QWidgetPrivate::get(d->viewport)->clipRect(); + const QPoint pos = d->draggedPosition - d->offset(); + const QRect area = QWidgetPrivate::get(d->viewport)->clipRect(); // do the scrolling if we are in the scroll margins if (pos.y() - area.top() < margin) @@ -4011,6 +4012,14 @@ void QAbstractItemView::doAutoScroll() d->dropIndicatorRect = QRect(); d->dropIndicatorPosition = QAbstractItemView::OnViewport; #endif + if (state() == QAbstractItemView::DragSelectingState) { + const QPoint globalPos = d->viewport->mapToGlobal(pos); + const QPoint windowPos = window()->mapFromGlobal(globalPos); + QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos, + Qt::NoButton, Qt::LeftButton, d->pressedModifiers, + Qt::MouseEventSynthesizedByQt); + QApplication::sendEvent(viewport(), &mm); + } d->viewport->update(); } } diff --git a/src/widgets/itemviews/qabstractitemview_p.h b/src/widgets/itemviews/qabstractitemview_p.h index 74850de992..14fae86c87 100644 --- a/src/widgets/itemviews/qabstractitemview_p.h +++ b/src/widgets/itemviews/qabstractitemview_p.h @@ -375,6 +375,7 @@ public: QPersistentModelIndex currentSelectionStartIndex; Qt::KeyboardModifiers pressedModifiers; QPoint pressedPosition; + QPoint draggedPosition; bool pressedAlreadySelected; bool releaseFromDoubleClick; diff --git a/tests/auto/widgets/itemviews/qabstractitemview/qtlogo.png b/tests/auto/widgets/itemviews/qabstractitemview/qtlogo.png new file mode 100644 index 0000000000000000000000000000000000000000..4f68e162de4295f123c6979c0f6f422031bc5603 GIT binary patch literal 2991 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 literal 0 HcmV?d00001 diff --git a/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp b/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp index d904f97d82..547d120bca 100644 --- a/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp +++ b/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp @@ -166,6 +166,8 @@ private slots: void scrollerSmoothScroll(); void inputMethodOpensEditor_data(); void inputMethodOpensEditor(); + void selectionAutoScrolling_data(); + void selectionAutoScrolling(); private: static QAbstractItemView *viewFromString(const QByteArray &viewType, QWidget *parent = nullptr) @@ -3177,5 +3179,120 @@ void tst_QAbstractItemView::inputMethodOpensEditor() } } +void tst_QAbstractItemView::selectionAutoScrolling_data() +{ + QTest::addColumn("orientation"); + QTest::addColumn("direction"); // negative or positive + + QTest::addRow("scroll up") << Qt::Vertical << -1; + QTest::addRow("scroll left") << Qt::Horizontal << -1; + QTest::addRow("scroll down") << Qt::Vertical << +1; + QTest::addRow("scroll right") << Qt::Horizontal << +1; +} + +void tst_QAbstractItemView::selectionAutoScrolling() +{ + QFETCH(Qt::Orientation, orientation); + QFETCH(int, direction); + + QListView listview; + listview.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + listview.setResizeMode(QListView::Fixed); + listview.setAutoScroll(true); + listview.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listview.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listview.setSpacing(10); + listview.setGeometry(0, 0, 500, 500); + listview.setFrameShape(QFrame::Shape::NoFrame); + listview.setEditTriggers(QListView::NoEditTriggers); + + QStandardItemModel *listModel = new QStandardItemModel(&listview); + listview.setModel(listModel); + listview.setViewMode(QListView::IconMode); + listview.setSelectionMode(QListView::ExtendedSelection); + + QPixmap pm(50, 50); + pm.fill(Qt::red); + for (int i = 0; i < 80; i++) { + QStandardItem *item = new QStandardItem(pm, QString::number(i)); + listModel->appendRow(item); + } + + listview.show(); + QVERIFY(QTest::qWaitForWindowExposed(&listview)); + + listview.resize(200, 200); + // scroll to the middle + listview.verticalScrollBar()->setValue(listview.verticalScrollBar()->maximum() / 2); + listview.horizontalScrollBar()->setValue(listview.horizontalScrollBar()->maximum() / 2); + + // remove all visible items so that we don't select any items at the edges, as that + // would scroll the view already + for (int x = 0; x < listview.viewport()->width(); x += 5) { + for (int y = 0; y < listview.viewport()->height(); y += 5) { + const QModelIndex index = listview.indexAt(QPoint(x, y)); + if (index.isValid()) + delete listModel->itemFromIndex(index); + } + } + // remove all items around the edges of the model + QRect topLeftRect = listview.visualRect(listModel->index(0, 0)); + const QPoint topLeftCenter(topLeftRect.center()); + QPoint bottomRightCenter; + for (int x = 0; x < listview.horizontalScrollBar()->maximum() + listview.viewport()->width(); x += 5) { + const QModelIndex index = listview.indexAt(topLeftCenter + QPoint(x, 0)); + if (index.isValid()) { + delete listModel->itemFromIndex(index); + bottomRightCenter.rx() = x; + } + } + for (int y = 0; y < listview.verticalScrollBar()->maximum() + listview.viewport()->height(); y += 5) { + const QModelIndex index = listview.indexAt(topLeftCenter + QPoint(0, y)); + if (index.isValid()) { + delete listModel->itemFromIndex(index); + bottomRightCenter.ry() = y; + } + } + for (int x = 0; x < bottomRightCenter.x(); x += 5) { + const QModelIndex index = listview.indexAt(topLeftCenter + QPoint(x, bottomRightCenter.y())); + if (index.isValid()) + delete listModel->itemFromIndex(index); + } + for (int y = 0; y < bottomRightCenter.y(); y += 5) { + const QModelIndex index = listview.indexAt(topLeftCenter + QPoint(bottomRightCenter.x(), y)); + if (index.isValid()) + delete listModel->itemFromIndex(index); + } + + + // Simulate multiple select behavior; start in the middle, drag to the edge + const QPoint pressPoint(listview.viewport()->width() / 2, listview.viewport()->height() / 2); + QPoint dragPoint = pressPoint; + if (orientation == Qt::Vertical) { + dragPoint.rx() += 50; + dragPoint.ry() = direction > 0 ? listview.viewport()->height() : 0; + } else { + dragPoint.rx() = direction > 0 ? listview.viewport()->width() : 0; + dragPoint.ry() += 50; + } + + QTest::mousePress(listview.viewport(), Qt::LeftButton, Qt::NoModifier, pressPoint); + QMouseEvent mmEvent(QEvent::MouseMove, dragPoint, Qt::NoButton, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(listview.viewport(), &mmEvent); // QTest::mouseMove is useless + + // check that we scrolled to the end + QScrollBar *scrollBar = orientation == Qt::Vertical + ? listview.verticalScrollBar() + : listview.horizontalScrollBar(); + + if (direction < 0) + QTRY_COMPARE(scrollBar->value(), 0); + else + QTRY_COMPARE(scrollBar->value(), scrollBar->maximum()); + QVERIFY(listview.selectionModel()->selectedIndexes().count() > 0); + + QTest::mouseRelease(listview.viewport(), Qt::LeftButton, Qt::NoModifier, dragPoint); +} + QTEST_MAIN(tst_QAbstractItemView) #include "tst_qabstractitemview.moc"