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 <zhanghao@uniontech.com>
Fixes: QTBUG-96124
Change-Id: I426f786e5842ae9f9fb04e9d34dc6d3379a6207f
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
This commit is contained in:
Volker Hilsheimer 2021-10-21 12:03:57 +02:00
parent 7d133a5613
commit 71aaf831d1
4 changed files with 137 additions and 10 deletions

View File

@ -1798,8 +1798,7 @@ void QAbstractItemView::mousePressEvent(QMouseEvent *event)
// this is the mouse press event that closed the last editor (via focus event) // this is the mouse press event that closed the last editor (via focus event)
d->pressClosedEditor = d->pressClosedEditorWatcher.isActive() && d->lastEditedIndex == index; d->pressClosedEditor = d->pressClosedEditorWatcher.isActive() && d->lastEditedIndex == index;
if (!d->selectionModel if (!d->selectionModel || (d->state == EditingState && d->hasEditor(index)))
|| (d->state == EditingState && d->hasEditor(index)))
return; return;
d->pressedAlreadySelected = d->selectionModel->isSelected(index); d->pressedAlreadySelected = d->selectionModel->isSelected(index);
@ -1808,10 +1807,9 @@ void QAbstractItemView::mousePressEvent(QMouseEvent *event)
QItemSelectionModel::SelectionFlags command = selectionCommand(index, event); QItemSelectionModel::SelectionFlags command = selectionCommand(index, event);
d->noSelectionOnMousePress = command == QItemSelectionModel::NoUpdate || !index.isValid(); d->noSelectionOnMousePress = command == QItemSelectionModel::NoUpdate || !index.isValid();
QPoint offset = d->offset(); QPoint offset = d->offset();
d->pressedPosition = pos + offset; d->pressedPosition = d->draggedPosition = pos + offset;
if ((command & QItemSelectionModel::Current) == 0) { if (!(command & QItemSelectionModel::Current))
d->currentSelectionStartIndex = index; d->currentSelectionStartIndex = index;
}
else if (!d->currentSelectionStartIndex.isValid()) else if (!d->currentSelectionStartIndex.isValid())
d->currentSelectionStartIndex = currentIndex(); d->currentSelectionStartIndex = currentIndex();
@ -1863,6 +1861,8 @@ void QAbstractItemView::mouseMoveEvent(QMouseEvent *event)
QPoint topLeft; QPoint topLeft;
QPoint bottomRight = event->position().toPoint(); QPoint bottomRight = event->position().toPoint();
d->draggedPosition = bottomRight + d->offset();
if (state() == ExpandingState || state() == CollapsingState) if (state() == ExpandingState || state() == CollapsingState)
return; return;
@ -1922,10 +1922,10 @@ void QAbstractItemView::mouseMoveEvent(QMouseEvent *event)
setSelection(selectionRect, command); setSelection(selectionRect, command);
// set at the end because it might scroll the view // set at the end because it might scroll the view
if (index.isValid() if (index.isValid() && (index != d->selectionModel->currentIndex()) && d->isIndexEnabled(index))
&& (index != d->selectionModel->currentIndex())
&& d->isIndexEnabled(index))
d->selectionModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); 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) void QAbstractItemView::dragMoveEvent(QDragMoveEvent *event)
{ {
Q_D(QAbstractItemView); Q_D(QAbstractItemView);
d->draggedPosition = event->position().toPoint() + d->offset();
if (dragDropMode() == InternalMove if (dragDropMode() == InternalMove
&& (event->source() != this || !(event->possibleActions() & Qt::MoveAction))) && (event->source() != this || !(event->possibleActions() & Qt::MoveAction)))
return; return;
@ -3989,8 +3990,8 @@ void QAbstractItemView::doAutoScroll()
int verticalValue = verticalScroll->value(); int verticalValue = verticalScroll->value();
int horizontalValue = horizontalScroll->value(); int horizontalValue = horizontalScroll->value();
QPoint pos = d->viewport->mapFromGlobal(QCursor::pos()); const QPoint pos = d->draggedPosition - d->offset();
QRect area = QWidgetPrivate::get(d->viewport)->clipRect(); const QRect area = QWidgetPrivate::get(d->viewport)->clipRect();
// do the scrolling if we are in the scroll margins // do the scrolling if we are in the scroll margins
if (pos.y() - area.top() < margin) if (pos.y() - area.top() < margin)
@ -4011,6 +4012,14 @@ void QAbstractItemView::doAutoScroll()
d->dropIndicatorRect = QRect(); d->dropIndicatorRect = QRect();
d->dropIndicatorPosition = QAbstractItemView::OnViewport; d->dropIndicatorPosition = QAbstractItemView::OnViewport;
#endif #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(); d->viewport->update();
} }
} }

View File

@ -375,6 +375,7 @@ public:
QPersistentModelIndex currentSelectionStartIndex; QPersistentModelIndex currentSelectionStartIndex;
Qt::KeyboardModifiers pressedModifiers; Qt::KeyboardModifiers pressedModifiers;
QPoint pressedPosition; QPoint pressedPosition;
QPoint draggedPosition;
bool pressedAlreadySelected; bool pressedAlreadySelected;
bool releaseFromDoubleClick; bool releaseFromDoubleClick;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -166,6 +166,8 @@ private slots:
void scrollerSmoothScroll(); void scrollerSmoothScroll();
void inputMethodOpensEditor_data(); void inputMethodOpensEditor_data();
void inputMethodOpensEditor(); void inputMethodOpensEditor();
void selectionAutoScrolling_data();
void selectionAutoScrolling();
private: private:
static QAbstractItemView *viewFromString(const QByteArray &viewType, QWidget *parent = nullptr) static QAbstractItemView *viewFromString(const QByteArray &viewType, QWidget *parent = nullptr)
@ -3177,5 +3179,120 @@ void tst_QAbstractItemView::inputMethodOpensEditor()
} }
} }
void tst_QAbstractItemView::selectionAutoScrolling_data()
{
QTest::addColumn<Qt::Orientation>("orientation");
QTest::addColumn<int>("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) QTEST_MAIN(tst_QAbstractItemView)
#include "tst_qabstractitemview.moc" #include "tst_qabstractitemview.moc"