diff options
-rw-r--r-- | src/quick/items/qquicktableview.cpp | 88 | ||||
-rw-r--r-- | src/quick/items/qquicktableview_p_p.h | 7 | ||||
-rw-r--r-- | tests/auto/quick/qquicktableview/tst_qquicktableview.cpp | 97 |
3 files changed, 167 insertions, 25 deletions
diff --git a/src/quick/items/qquicktableview.cpp b/src/quick/items/qquicktableview.cpp index e9d0ab8f3a..2ae006be29 100644 --- a/src/quick/items/qquicktableview.cpp +++ b/src/quick/items/qquicktableview.cpp @@ -587,6 +587,16 @@ void QQuickTableViewPrivate::enforceTableAtOrigin() } } +void QQuickTableViewPrivate::updateAverageEdgeSize() +{ + int bottomCell = loadedTable.bottom(); + int rightCell = loadedTable.right(); + qreal accRowSpacing = bottomCell * cellSpacing.height(); + qreal accColumnSpacing = rightCell * cellSpacing.width(); + averageEdgeSize.setHeight((loadedTableOuterRect.bottom() - accRowSpacing) / (bottomCell + 1)); + averageEdgeSize.setWidth((loadedTableOuterRect.right() - accColumnSpacing) / (rightCell + 1)); +} + void QQuickTableViewPrivate::syncLoadedTableRectFromLoadedTable() { QRectF topLeftRect = loadedTableItem(loadedTable.topLeft())->geometry(); @@ -1172,14 +1182,16 @@ void QQuickTableViewPrivate::processLoadRequest() syncLoadedTableFromLoadRequest(); layoutTableEdgeFromLoadRequest(); - syncLoadedTableRectFromLoadedTable(); - enforceTableAtOrigin(); - updateContentWidth(); - updateContentHeight(); + + if (rebuildState == RebuildState::Done) { + enforceTableAtOrigin(); + updateContentWidth(); + updateContentHeight(); + drainReusePoolAfterLoadRequest(); + } loadRequest.markAsDone(); - drainReusePoolAfterLoadRequest(); qCDebug(lcTableViewDelegateLifecycle()) << "request completed! Table:" << tableLayoutToString(); } @@ -1260,23 +1272,34 @@ void QQuickTableViewPrivate::beginRebuildTable() if (loadRequest.isActive()) cancelLoadRequest(); + calculateTableSize(); + QPoint topLeft; QPointF topLeftPos; - calculateTableSize(); if (rebuildOptions & RebuildOption::All) { + qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::All"; releaseLoadedItems(QQmlTableInstanceModel::NotReusable); - topLeft = QPoint(0, 0); - topLeftPos = QPoint(0, 0); } else if (rebuildOptions & RebuildOption::ViewportOnly) { - // Rebuild the table without flicking the content view back to origin, and - // start building from the same top left item that is currently showing - // (unless it has been removed from the model). + qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::ViewportOnly"; releaseLoadedItems(reusableFlag); - topLeft = loadedTable.topLeft(); - topLeftPos = loadedTableOuterRect.topLeft(); - topLeft.setX(qMin(topLeft.x(), tableSize.width() - 1)); - topLeft.setY(qMin(topLeft.y(), tableSize.height() - 1)); + + if (rebuildOptions & RebuildOption::CalculateNewTopLeftRow) { + const int newRow = int(viewportRect.y() / (averageEdgeSize.height() + cellSpacing.height())); + topLeft.ry() = qBound(0, newRow, tableSize.height() - 1); + topLeftPos.ry() = topLeft.y() * (averageEdgeSize.height() + cellSpacing.height()); + } else { + topLeft.ry() = qBound(0, loadedTable.topLeft().y(), tableSize.height() - 1); + topLeftPos.ry() = loadedTableOuterRect.topLeft().y(); + } + if (rebuildOptions & RebuildOption::CalculateNewTopLeftColumn) { + const int newColumn = int(viewportRect.x() / (averageEdgeSize.width() + cellSpacing.width())); + topLeft.rx() = qBound(0, newColumn, tableSize.width() - 1); + topLeftPos.rx() = topLeft.x() * (averageEdgeSize.width() + cellSpacing.width()); + } else { + topLeft.rx() = qBound(0, loadedTable.topLeft().x(), tableSize.width() - 1); + topLeftPos.rx() = loadedTableOuterRect.topLeft().x(); + } } else { Q_TABLEVIEW_UNREACHABLE(rebuildOptions); } @@ -1301,6 +1324,10 @@ void QQuickTableViewPrivate::layoutAfterLoadingInitialTable() // available yet for the calculation. So we do it now. relayoutTable(); } + + updateAverageEdgeSize(); + updateContentWidth(); + updateContentHeight(); } void QQuickTableViewPrivate::loadInitialTopLeftItem(const QPoint &cell, const QPointF &pos) @@ -1920,13 +1947,23 @@ void QQuickTableView::viewportMoved(Qt::Orientations orientation) Q_D(QQuickTableView); QQuickFlickable::viewportMoved(orientation); - // Calling polish() will schedule a polish event. But while the user is flicking, several - // mouse events will be handled before we get an updatePolish() call. And the updatePolish() - // call will only see the last mouse position. This results in a stuttering flick experience - // (especially on windows). We improve on this by calling updatePolish() directly. But this - // has the pitfall that we open up for recursive callbacks. E.g while inside updatePolish(), we - // load/unload items, and emit signals. The application can listen to those signals and set a - // new contentX/Y on the flickable. So we need to guard for this, to avoid unexpected behavior. + QQuickTableViewPrivate::RebuildOptions options = QQuickTableViewPrivate::RebuildOption::None; + + // Check the viewport moved more than one page vertically + if (!d->viewportRect.intersects(QRectF(d->viewportRect.x(), contentY(), 1, height()))) + options |= QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftRow; + // Check the viewport moved more than one page horizontally + if (!d->viewportRect.intersects(QRectF(contentX(), d->viewportRect.y(), width(), 1))) + options |= QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftColumn; + + if (options) { + // When the viewport has moved more than one page vertically or horizontally, we switch + // strategy from refilling edges around the current table to instead rebuild the table + // from scratch inside the new viewport. This will greatly improve performance when flicking + // a long distance in one go, which can easily happen when dragging on scrollbars. + options |= QQuickTableViewPrivate::RebuildOption::ViewportOnly; + d->scheduleRebuildTable(options); + } if (d->rebuildScheduled) { // No reason to do anything, since we're about to rebuild the whole table anyway. @@ -1936,6 +1973,13 @@ void QQuickTableView::viewportMoved(Qt::Orientations orientation) return; } + // Calling polish() will schedule a polish event. But while the user is flicking, several + // mouse events will be handled before we get an updatePolish() call. And the updatePolish() + // call will only see the last mouse position. This results in a stuttering flick experience + // (especially on windows). We improve on this by calling updatePolish() directly. But this + // has the pitfall that we open up for recursive callbacks. E.g while inside updatePolish(), we + // load/unload items, and emit signals. The application can listen to those signals and set a + // new contentX/Y on the flickable. So we need to guard for this, to avoid unexpected behavior. if (d->polishing) polish(); else diff --git a/src/quick/items/qquicktableview_p_p.h b/src/quick/items/qquicktableview_p_p.h index 4f5c819887..a4f829addd 100644 --- a/src/quick/items/qquicktableview_p_p.h +++ b/src/quick/items/qquicktableview_p_p.h @@ -182,7 +182,9 @@ public: enum class RebuildOption { None = 0, ViewportOnly = 0x1, - All = 0x2, + CalculateNewTopLeftRow = 0x2, + CalculateNewTopLeftColumn = 0x4, + All = 0x8, }; Q_DECLARE_FLAGS(RebuildOptions, RebuildOption) @@ -257,6 +259,8 @@ public: QQmlNullableValue<qreal> explicitContentWidth; QQmlNullableValue<qreal> explicitContentHeight; + QSizeF averageEdgeSize; + const static QPoint kLeft; const static QPoint kRight; const static QPoint kUp; @@ -289,6 +293,7 @@ public: void updateContentWidth(); void updateContentHeight(); + void updateAverageEdgeSize(); void enforceTableAtOrigin(); void syncLoadedTableRectFromLoadedTable(); diff --git a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp index 039fb91da0..60b938d127 100644 --- a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp +++ b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp @@ -117,6 +117,7 @@ private slots: void checkRowHeightProviderNotCallable(); void checkForceLayoutFunction(); void checkContentWidthAndHeight(); + void checkPageFlicking(); void checkExplicitContentWidthAndHeight(); void checkContentXY(); void noDelegate(); @@ -554,8 +555,15 @@ void tst_QQuickTableView::checkContentWidthAndHeight() tableView->setContentX(flickTo); tableView->setContentY(flickTo); + // Since we move the viewport more than a page, tableview + // will jump to the new position and do a rebuild. + QVERIFY(tableViewPrivate->polishScheduled); + QVERIFY(tableViewPrivate->rebuildScheduled); + WAIT_UNTIL_POLISHED; + const int largeSizeCellCountInView = qCeil(tableView->width() / cellSizeLarge); const int columnCount = smallCellCount + largeSizeCellCountInView; + QCOMPARE(tableViewPrivate->loadedTable.left(), smallCellCount); QCOMPARE(tableViewPrivate->loadedTable.right(), columnCount - 1); const qreal firstHalfLength = smallCellCount * cellSizeSmall; @@ -572,8 +580,20 @@ void tst_QQuickTableView::checkContentWidthAndHeight() // check that we then end up with the exact content width/height. const qreal secondHalfLength = largeCellCount * cellSizeLarge; const qreal expectedFullSize = (firstHalfLength + secondHalfLength) + accumulatedSpacing; - tableView->setContentX(expectedFullSize); - tableView->setContentY(expectedFullSize); + + // If we flick more than one page at a time, tableview will jump to the new + // position and rebuild the table without loading the edges in-between. Which + // row and column that ends up as new top-left is then based on a prediction, and + // therefore unreliable. To avoid this to happen (which will also affect the + // reported size of the table), we flick to the end position in smaller chuncks. + QVERIFY(!tableViewPrivate->polishScheduled); + QVERIFY(!tableViewPrivate->rebuildScheduled); + int pages = qCeil((expectedFullSize - tableView->contentX()) / tableView->width()); + for (int i = 0; i < pages; i++) { + tableView->setContentX(tableView->contentX() + tableView->width() - 1); + tableView->setContentY(tableView->contentY() + tableView->height() - 1); + QVERIFY(!tableViewPrivate->rebuildScheduled); + } QCOMPARE(tableView->contentWidth(), expectedFullSize); QCOMPARE(tableView->contentHeight(), expectedFullSize); @@ -587,6 +607,79 @@ void tst_QQuickTableView::checkContentWidthAndHeight() QCOMPARE(tableView->contentHeight(), expectedFullSize); } +void tst_QQuickTableView::checkPageFlicking() +{ + // Check that we rebuild the table instead of refilling edges, if the viewport moves + // more than a page (the size of TableView). + LOAD_TABLEVIEW("plaintableview.qml"); + + const int cellWidth = 100; + const int cellHeight = 50; + auto model = TestModelAsVariant(10000, 10000); + + tableView->setModel(model); + + WAIT_UNTIL_POLISHED; + + // Sanity check startup table + QRect tableRect = tableViewPrivate->loadedTable; + QCOMPARE(tableRect.x(), 0); + QCOMPARE(tableRect.y(), 0); + QCOMPARE(tableRect.width(), tableView->width() / cellWidth); + QCOMPARE(tableRect.height(), tableView->height() / cellHeight); + + // Since all cells have the same size, the average row/column + // size found by TableView should be exactly equal to this. + QCOMPARE(tableViewPrivate->averageEdgeSize.width(), cellWidth); + QCOMPARE(tableViewPrivate->averageEdgeSize.height(), cellHeight); + + QVERIFY(!tableViewPrivate->rebuildScheduled); + QCOMPARE(tableViewPrivate->scheduledRebuildOptions, QQuickTableViewPrivate::RebuildOption::None); + + // Flick 5000 columns to the right, and check that this triggers a + // rebuild, and that we end up at the expected top-left. + const int flickToColumn = 5000; + const qreal columnSpacing = tableView->columnSpacing(); + const qreal flickToColumnInPixels = ((cellWidth + columnSpacing) * flickToColumn) - columnSpacing; + tableView->setContentX(flickToColumnInPixels); + + QVERIFY(tableViewPrivate->rebuildScheduled); + QVERIFY(tableViewPrivate->scheduledRebuildOptions & QQuickTableViewPrivate::RebuildOption::ViewportOnly); + QVERIFY(tableViewPrivate->scheduledRebuildOptions & QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftColumn); + QVERIFY(!(tableViewPrivate->scheduledRebuildOptions & QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftRow)); + + WAIT_UNTIL_POLISHED; + + tableRect = tableViewPrivate->loadedTable; + QCOMPARE(tableRect.x(), flickToColumn); + QCOMPARE(tableRect.y(), 0); + QCOMPARE(tableRect.width(), tableView->width() / cellWidth); + QCOMPARE(tableRect.height(), tableView->height() / cellHeight); + + // Flick 5000 rows down as well. Since flicking down should only calculate a new row (but + // keep the current column), we deliberatly change the average width to check that it's + // actually ignored by the rebuild, and that the column stays the same. + tableViewPrivate->averageEdgeSize.rwidth() /= 2; + + const int flickToRow = 5000; + const qreal rowSpacing = tableView->rowSpacing(); + const qreal flickToRowInPixels = ((cellHeight + rowSpacing) * flickToRow) - rowSpacing; + tableView->setContentY(flickToRowInPixels); + + QVERIFY(tableViewPrivate->rebuildScheduled); + QVERIFY(tableViewPrivate->scheduledRebuildOptions & QQuickTableViewPrivate::RebuildOption::ViewportOnly); + QVERIFY(!(tableViewPrivate->scheduledRebuildOptions & QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftColumn)); + QVERIFY(tableViewPrivate->scheduledRebuildOptions & QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftRow); + + WAIT_UNTIL_POLISHED; + + tableRect = tableViewPrivate->loadedTable; + QCOMPARE(tableRect.x(), flickToRow); + QCOMPARE(tableRect.y(), flickToColumn); + QCOMPARE(tableRect.width(), tableView->width() / cellWidth); + QCOMPARE(tableRect.height(), tableView->height() / cellHeight); +} + void tst_QQuickTableView::checkExplicitContentWidthAndHeight() { // Check that you can set a custom contentWidth/Height, and that |