1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtQuick module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qquicktableview_p.h"
41 #include "qquicktableview_p_p.h"
42 
43 #include <QtCore/qtimer.h>
44 #include <QtCore/qdir.h>
45 #include <QtQmlModels/private/qqmldelegatemodel_p.h>
46 #include <QtQmlModels/private/qqmldelegatemodel_p_p.h>
47 #include <QtQml/private/qqmlincubator_p.h>
48 #include <QtQmlModels/private/qqmlchangeset_p.h>
49 #include <QtQml/qqmlinfo.h>
50 
51 #include <QtQuick/private/qquickflickable_p_p.h>
52 #include <QtQuick/private/qquickitemviewfxitem_p_p.h>
53 
54 /*!
55     \qmltype TableView
56     \inqmlmodule QtQuick
57     \since 5.12
58     \ingroup qtquick-views
59     \inherits Flickable
60     \brief Provides a table view of items to display data from a model.
61 
62     A TableView has a \l model that defines the data to be displayed, and a
63     \l delegate that defines how the data should be displayed.
64 
65     TableView inherits \l Flickable. This means that while the model can have
66     any number of rows and columns, only a subsection of the table is usually
67     visible inside the viewport. As soon as you flick, new rows and columns
68     enter the viewport, while old ones exit and are removed from the viewport.
69     The rows and columns that move out are reused for building the rows and columns
70     that move into the viewport. As such, the TableView support models of any
71     size without affecting performance.
72 
73     A TableView displays data from models created from built-in QML types
74     such as ListModel and XmlListModel, which populates the first column only
75     in a TableView. To create models with multiple columns, either use
76     \l TableModel or a C++ model that inherits QAbstractItemModel.
77 
78     \section1 Example Usage
79 
80     \section2 C++ Models
81 
82     The following example shows how to create a model from C++ with multiple
83     columns:
84 
85     \snippet qml/tableview/cpp-tablemodel.h 0
86 
87     And then how to use it from QML:
88 
89     \snippet qml/tableview/cpp-tablemodel.qml 0
90 
91     \section2 QML Models
92 
93     For prototyping and displaying very simple data (from a web API, for
94     example), \l TableModel can be used:
95 
96     \snippet qml/tableview/qml-tablemodel.qml 0
97 
98     \section1 Reusing items
99 
100     TableView recycles delegate items by default, instead of instantiating from
101     the \l delegate whenever new rows and columns are flicked into view. This
102     approach gives a huge performance boost, depending on the complexity of the
103     delegate.
104 
105     When an item is flicked out, it moves to the \e{reuse pool}, which is an
106     internal cache of unused items. When this happens, the \l TableView::pooled
107     signal is emitted to inform the item about it. Likewise, when the item is
108     moved back from the pool, the \l TableView::reused signal is emitted.
109 
110     Any item properties that come from the model are updated when the
111     item is reused. This includes \c index, \c row, and \c column, but also
112     any model roles.
113 
114     \note Avoid storing any state inside a delegate. If you do, reset it
115     manually on receiving the \l TableView::reused signal.
116 
117     If an item has timers or animations, consider pausing them on receiving
118     the \l TableView::pooled signal. That way you avoid using the CPU resources
119     for items that are not visible. Likewise, if an item has resources that
120     cannot be reused, they could be freed up.
121 
122     If you don't want to reuse items or if the \l delegate cannot support it,
123     you can set the \l reuseItems property to \c false.
124 
125     \note While an item is in the pool, it might still be alive and respond
126     to connected signals and bindings.
127 
128     The following example shows a delegate that animates a spinning rectangle. When
129     it is pooled, the animation is temporarily paused:
130 
131     \snippet qml/tableview/reusabledelegate.qml 0
132 
133     \section1 Row heights and column widths
134 
135     When a new column is flicked into view, TableView will determine its width
136     by calling the \l columnWidthProvider function. TableView does not store
137     row height or column width, as it's designed to support large models
138     containing any number of rows and columns. Instead, it will ask the
139     application whenever it needs to know.
140 
141     TableView uses the largest \c implicitWidth among the items as the column
142     width, unless the \l columnWidthProvider property is explicitly set. Once
143     the column width is found, all other items in the same column are resized
144     to this width, even if new items that are flicked in later have larger
145     \c implicitWidth. Setting an explicit \c width on an item is ignored and
146     overwritten.
147 
148     \note The calculated width of a column is discarded when it is flicked out
149     of the viewport, and is recalculated if the column is flicked back in. The
150     calculation is always based on the items that are visible when the column
151     is flicked in. This means that column width can be different each time,
152     depending on which row you're at when the column enters. You should
153     therefore have the same \c implicitWidth for all items in a column, or set
154     \l columnWidthProvider. The same logic applies for the row height
155     calculation.
156 
157     If you change the values that a \l rowHeightProvider or a
158     \l columnWidthProvider return for rows and columns inside the viewport, you
159     must call \l forceLayout. This informs TableView that it needs to use the
160     provider functions again to recalculate and update the layout.
161 
162     Since Qt 5.13, if you want to hide a specific column, you can return \c 0
163     from the \l columnWidthProvider for that column. Likewise, you can return 0
164     from the \l rowHeightProvider to hide a row. If you return a negative
165     number, TableView will fall back to calculate the size based on the delegate
166     items.
167 
168     \note The size of a row or column should be a whole number to avoid
169     sub-pixel alignment of items.
170 
171     The following example shows how to set a simple \c columnWidthProvider
172     together with a timer that modifies the values the function returns. When
173     the array is modified, \l forceLayout is called to let the changes
174     take effect:
175 
176     \snippet qml/tableview/tableviewwithprovider.qml 0
177 
178     \section1 Overlays and underlays
179 
180     All new items that are instantiated from the delegate are parented to the
181     \l{Flickable::}{contentItem} with the \c z value, \c 1. You can add your
182     own items inside the Tableview, as child items of the Flickable. By
183     controlling their \c z value, you can make them be on top of or
184     underneath the table items.
185 
186     Here is an example that shows how to add some text on top of the table, that
187     moves together with the table as you flick:
188 
189     \snippet qml/tableview/tableviewwithheader.qml 0
190 */
191 
192 /*!
193     \qmlproperty int QtQuick::TableView::rows
194     \readonly
195 
196     This property holds the number of rows in the table.
197 
198     \note \a rows is usually equal to the number of rows in the model, but can
199     temporarily differ until all pending model changes have been processed.
200 
201     This property is read only.
202 */
203 
204 /*!
205     \qmlproperty int QtQuick::TableView::columns
206     \readonly
207 
208     This property holds the number of columns in the table.
209 
210     \note \a columns is usually equal to the number of columns in the model, but
211     can temporarily differ until all pending model changes have been processed.
212 
213     If the model is a list, columns will be \c 1.
214 
215     This property is read only.
216 */
217 
218 /*!
219     \qmlproperty real QtQuick::TableView::rowSpacing
220 
221     This property holds the spacing between the rows.
222 
223     The default value is \c 0.
224 */
225 
226 /*!
227     \qmlproperty real QtQuick::TableView::columnSpacing
228 
229     This property holds the spacing between the columns.
230 
231     The default value is \c 0.
232 */
233 
234 /*!
235     \qmlproperty var QtQuick::TableView::rowHeightProvider
236 
237     This property can hold a function that returns the row height for each row
238     in the model. It is called whenever TableView needs to know the height of
239     a specific row. The function takes one argument, \c row, for which the
240     TableView needs to know the height.
241 
242     Since Qt 5.13, if you want to hide a specific row, you can return \c 0
243     height for that row. If you return a negative number, TableView calculates
244     the height based on the delegate items.
245 
246     \sa columnWidthProvider, {Row heights and column widths}
247 */
248 
249 /*!
250     \qmlproperty var QtQuick::TableView::columnWidthProvider
251 
252     This property can hold a function that returns the column width for each
253     column in the model. It is called whenever TableView needs to know the
254     width of a specific column. The function takes one argument, \c column,
255     for which the TableView needs to know the width.
256 
257     Since Qt 5.13, if you want to hide a specific column, you can return \c 0
258     width for that column. If you return a negative number, TableView
259     calculates the width based on the delegate items.
260 
261     \sa rowHeightProvider, {Row heights and column widths}
262 */
263 
264 /*!
265     \qmlproperty model QtQuick::TableView::model
266     This property holds the model that provides data for the table.
267 
268     The model provides the set of data that is used to create the items
269     in the view. Models can be created directly in QML using \l TableModel,
270     \l ListModel, \l XmlListModel, or \l ObjectModel, or provided by a custom
271     C++ model class. The C++ model must be a subclass of \l QAbstractItemModel
272     or a simple list.
273 
274     \sa {qml-data-models}{Data Models}
275 */
276 
277 /*!
278     \qmlproperty Component QtQuick::TableView::delegate
279 
280     The delegate provides a template defining each cell item instantiated by the
281     view. The model index is exposed as an accessible \c index property. The same
282     applies to \c row and \c column. Properties of the model are also available
283     depending upon the type of \l {qml-data-models}{Data Model}.
284 
285     A delegate should specify its size using \l{Item::}{implicitWidth} and
286     \l {Item::}{implicitHeight}. The TableView lays out the items based on that
287     information. Explicit width or height settings are ignored and overwritten.
288 
289     \note Delegates are instantiated as needed and may be destroyed at any time.
290     They are also reused if the \l reuseItems property is set to \c true. You
291     should therefore avoid storing state information in the delegates.
292 
293     \sa {Row heights and column widths}, {Reusing items}
294 */
295 
296 /*!
297     \qmlproperty bool QtQuick::TableView::reuseItems
298 
299     This property holds whether or not items instantiated from the \l delegate
300     should be reused. If set to \c false, any currently pooled items
301     are destroyed.
302 
303     \sa {Reusing items}, TableView::pooled, TableView::reused
304 */
305 
306 /*!
307     \qmlproperty real QtQuick::TableView::contentWidth
308 
309     This property holds the table width required to accommodate the number of
310     columns in the model. This is usually not the same as the \c width of the
311     \l view, which means that the table's width could be larger or smaller than
312     the viewport width. As a TableView cannot always know the exact width of
313     the table without loading all columns in the model, the \c contentWidth is
314     usually an estimate based on the initially loaded table.
315 
316     If you know what the width of the table will be, assign a value to
317     \c contentWidth, to avoid unnecessary calculations and updates to the
318     TableView.
319 
320     \sa contentHeight, columnWidthProvider
321 */
322 
323 /*!
324     \qmlproperty real QtQuick::TableView::contentHeight
325 
326     This property holds the table height required to accommodate the number of
327     rows in the data model. This is usually not the same as the \c height of the
328     \c view, which means that the table's height could be larger or smaller than the
329     viewport height. As a TableView cannot always know the exact height of the
330     table without loading all rows in the model, the \c contentHeight is
331     usually an estimate based on the initially loaded table.
332 
333     If you know what the height of the table will be, assign a
334     value to \c contentHeight, to avoid unnecessary calculations and updates to
335     the TableView.
336 
337     \sa contentWidth, rowHeightProvider
338 */
339 
340 /*!
341     \qmlmethod QtQuick::TableView::forceLayout
342 
343     Responding to changes in the model are batched so that they are handled
344     only once per frame. This means the TableView delays showing any changes
345     while a script is being run. The same is also true when changing
346     properties, such as \l rowSpacing or \l{Item::anchors.leftMargin}{leftMargin}.
347 
348     This method forces the TableView to immediately update the layout so
349     that any recent changes take effect.
350 
351     Calling this function re-evaluates the size and position of each visible
352     row and column. This is needed if the functions assigned to
353     \l rowHeightProvider or \l columnWidthProvider return different values than
354     what is already assigned.
355 */
356 
357 /*!
358     \qmlattachedproperty TableView QtQuick::TableView::view
359 
360     This attached property holds the view that manages the delegate instance.
361     It is attached to each instance of the delegate.
362 */
363 
364 /*!
365     \qmlattachedsignal QtQuick::TableView::pooled
366 
367     This signal is emitted after an item has been added to the reuse
368     pool. You can use it to pause ongoing timers or animations inside
369     the item, or free up resources that cannot be reused.
370 
371     This signal is emitted only if the \l reuseItems property is \c true.
372 
373     \sa {Reusing items}, reuseItems, reused
374 */
375 
376 /*!
377     \qmlattachedsignal QtQuick::TableView::reused
378 
379     This signal is emitted after an item has been reused. At this point, the
380     item has been taken out of the pool and placed inside the content view,
381     and the model properties such as index, row, and column have been updated.
382 
383     Other properties that are not provided by the model does not change when an
384     item is reused. You should avoid storing any state inside a delegate, but if
385     you do, manually reset that state on receiving this signal.
386 
387     This signal is emitted when the item is reused, and not the first time the
388     item is created.
389 
390     This signal is emitted only if the \l reuseItems property is \c true.
391 
392     \sa {Reusing items}, reuseItems, pooled
393 */
394 
395 QT_BEGIN_NAMESPACE
396 
397 Q_LOGGING_CATEGORY(lcTableViewDelegateLifecycle, "qt.quick.tableview.lifecycle")
398 
399 #define Q_TABLEVIEW_UNREACHABLE(output) { dumpTable(); qWarning() << "output:" << output; Q_UNREACHABLE(); }
400 #define Q_TABLEVIEW_ASSERT(cond, output) Q_ASSERT((cond) || [&](){ dumpTable(); qWarning() << "output:" << output; return false;}())
401 
402 static const Qt::Edge allTableEdges[] = { Qt::LeftEdge, Qt::RightEdge, Qt::TopEdge, Qt::BottomEdge };
403 static const int kEdgeIndexNotSet = -2;
404 static const int kEdgeIndexAtEnd = -3;
405 
406 const QPoint QQuickTableViewPrivate::kLeft = QPoint(-1, 0);
407 const QPoint QQuickTableViewPrivate::kRight = QPoint(1, 0);
408 const QPoint QQuickTableViewPrivate::kUp = QPoint(0, -1);
409 const QPoint QQuickTableViewPrivate::kDown = QPoint(0, 1);
410 
EdgeRange()411 QQuickTableViewPrivate::EdgeRange::EdgeRange()
412     : startIndex(kEdgeIndexNotSet)
413     , endIndex(kEdgeIndexNotSet)
414     , size(0)
415 {}
416 
containsIndex(Qt::Edge edge,int index)417 bool QQuickTableViewPrivate::EdgeRange::containsIndex(Qt::Edge edge, int index)
418 {
419     if (startIndex == kEdgeIndexNotSet)
420         return false;
421 
422     if (endIndex == kEdgeIndexAtEnd) {
423         switch (edge) {
424         case Qt::LeftEdge:
425         case Qt::TopEdge:
426             return index <= startIndex;
427         case Qt::RightEdge:
428         case Qt::BottomEdge:
429             return index >= startIndex;
430         }
431     }
432 
433     const int s = std::min(startIndex, endIndex);
434     const int e = std::max(startIndex, endIndex);
435     return index >= s && index <= e;
436 }
437 
QQuickTableViewPrivate()438 QQuickTableViewPrivate::QQuickTableViewPrivate()
439     : QQuickFlickablePrivate()
440 {
441     QObject::connect(&columnWidths, &QQuickTableSectionSizeProvider::sizeChanged,
442                             [this] { this->forceLayout();});
443     QObject::connect(&rowHeights, &QQuickTableSectionSizeProvider::sizeChanged,
444                             [this] { this->forceLayout();});
445 }
446 
~QQuickTableViewPrivate()447 QQuickTableViewPrivate::~QQuickTableViewPrivate()
448 {
449     for (auto *fxTableItem : loadedItems) {
450         if (auto item = fxTableItem->item) {
451             if (fxTableItem->ownItem)
452                 delete item;
453             else if (tableModel)
454                 tableModel->dispose(item);
455         }
456         delete fxTableItem;
457     }
458 
459     if (tableModel)
460         delete tableModel;
461 }
462 
tableLayoutToString() const463 QString QQuickTableViewPrivate::tableLayoutToString() const
464 {
465     if (loadedItems.isEmpty())
466         return QLatin1String("table is empty!");
467     return QString(QLatin1String("table cells: (%1,%2) -> (%3,%4), item count: %5, table rect: %6,%7 x %8,%9"))
468             .arg(leftColumn()).arg(topRow())
469             .arg(rightColumn()).arg(bottomRow())
470             .arg(loadedItems.count())
471             .arg(loadedTableOuterRect.x())
472             .arg(loadedTableOuterRect.y())
473             .arg(loadedTableOuterRect.width())
474             .arg(loadedTableOuterRect.height());
475 }
476 
dumpTable() const477 void QQuickTableViewPrivate::dumpTable() const
478 {
479     auto listCopy = loadedItems.values();
480     std::stable_sort(listCopy.begin(), listCopy.end(),
481         [](const FxTableItem *lhs, const FxTableItem *rhs)
482         { return lhs->index < rhs->index; });
483 
484     qWarning() << QStringLiteral("******* TABLE DUMP *******");
485     for (int i = 0; i < listCopy.count(); ++i)
486         qWarning() << static_cast<FxTableItem *>(listCopy.at(i))->cell;
487     qWarning() << tableLayoutToString();
488 
489     const QString filename = QStringLiteral("QQuickTableView_dumptable_capture.png");
490     const QString path = QDir::current().absoluteFilePath(filename);
491     if (q_func()->window() && q_func()->window()->grabWindow().save(path))
492         qWarning() << "Window capture saved to:" << path;
493 }
494 
getAttachedObject(const QObject * object) const495 QQuickTableViewAttached *QQuickTableViewPrivate::getAttachedObject(const QObject *object) const
496 {
497     QObject *attachedObject = qmlAttachedPropertiesObject<QQuickTableView>(object);
498     return static_cast<QQuickTableViewAttached *>(attachedObject);
499 }
500 
modelIndexAtCell(const QPoint & cell) const501 int QQuickTableViewPrivate::modelIndexAtCell(const QPoint &cell) const
502 {
503     // QQmlTableInstanceModel expects index to be in column-major
504     // order. This means that if the view is transposed (with a flipped
505     // width and height), we need to calculate it in row-major instead.
506     if (isTransposed) {
507         int availableColumns = tableSize.width();
508         return (cell.y() * availableColumns) + cell.x();
509     } else {
510         int availableRows = tableSize.height();
511         return (cell.x() * availableRows) + cell.y();
512     }
513 }
514 
cellAtModelIndex(int modelIndex) const515 QPoint QQuickTableViewPrivate::cellAtModelIndex(int modelIndex) const
516 {
517     // QQmlTableInstanceModel expects index to be in column-major
518     // order. This means that if the view is transposed (with a flipped
519     // width and height), we need to calculate it in row-major instead.
520     if (isTransposed) {
521         int availableColumns = tableSize.width();
522         int row = int(modelIndex / availableColumns);
523         int column = modelIndex % availableColumns;
524         return QPoint(column, row);
525     } else {
526         int availableRows = tableSize.height();
527         int column = int(modelIndex / availableRows);
528         int row = modelIndex % availableRows;
529         return QPoint(column, row);
530     }
531 }
532 
edgeToArrayIndex(Qt::Edge edge)533 int QQuickTableViewPrivate::edgeToArrayIndex(Qt::Edge edge)
534 {
535     return int(log2(float(edge)));
536 }
537 
clearEdgeSizeCache()538 void QQuickTableViewPrivate::clearEdgeSizeCache()
539 {
540     cachedColumnWidth.startIndex = kEdgeIndexNotSet;
541     cachedRowHeight.startIndex = kEdgeIndexNotSet;
542 
543     for (Qt::Edge edge : allTableEdges)
544         cachedNextVisibleEdgeIndex[edgeToArrayIndex(edge)].startIndex = kEdgeIndexNotSet;
545 }
546 
nextVisibleEdgeIndexAroundLoadedTable(Qt::Edge edge)547 int QQuickTableViewPrivate::nextVisibleEdgeIndexAroundLoadedTable(Qt::Edge edge)
548 {
549     // Find the next column (or row) around the loaded table that is
550     // visible, and should be loaded next if the content item moves.
551     int startIndex = -1;
552     switch (edge) {
553     case Qt::LeftEdge: startIndex = loadedColumns.firstKey() - 1; break;
554     case Qt::RightEdge: startIndex = loadedColumns.lastKey() + 1; break;
555     case Qt::TopEdge: startIndex = loadedRows.firstKey() - 1; break;
556     case Qt::BottomEdge: startIndex = loadedRows.lastKey() + 1; break;
557     }
558 
559     return nextVisibleEdgeIndex(edge, startIndex);
560 }
561 
nextVisibleEdgeIndex(Qt::Edge edge,int startIndex)562 int QQuickTableViewPrivate::nextVisibleEdgeIndex(Qt::Edge edge, int startIndex)
563 {
564     // First check if we have already searched for the first visible index
565     // after the given startIndex recently, and if so, return the cached result.
566     // The cached result is valid if startIndex is inside the range between the
567     // startIndex and the first visible index found after it.
568     auto &cachedResult = cachedNextVisibleEdgeIndex[edgeToArrayIndex(edge)];
569     if (cachedResult.containsIndex(edge, startIndex))
570         return cachedResult.endIndex;
571 
572     // Search for the first column (or row) in the direction of edge that is
573     // visible, starting from the given column (startIndex).
574     int foundIndex = kEdgeIndexNotSet;
575     int testIndex = startIndex;
576 
577     switch (edge) {
578     case Qt::LeftEdge: {
579         forever {
580             if (testIndex < 0) {
581                 foundIndex = kEdgeIndexAtEnd;
582                 break;
583             }
584 
585             if (!isColumnHidden(testIndex)) {
586                 foundIndex = testIndex;
587                 break;
588             }
589 
590             --testIndex;
591         }
592         break; }
593     case Qt::RightEdge: {
594         forever {
595             if (testIndex > tableSize.width() - 1) {
596                 foundIndex = kEdgeIndexAtEnd;
597                 break;
598             }
599 
600             if (!isColumnHidden(testIndex)) {
601                 foundIndex = testIndex;
602                 break;
603             }
604 
605             ++testIndex;
606         }
607         break; }
608     case Qt::TopEdge: {
609         forever {
610             if (testIndex < 0) {
611                 foundIndex = kEdgeIndexAtEnd;
612                 break;
613             }
614 
615             if (!isRowHidden(testIndex)) {
616                 foundIndex = testIndex;
617                 break;
618             }
619 
620             --testIndex;
621         }
622         break; }
623     case Qt::BottomEdge: {
624         forever {
625             if (testIndex > tableSize.height() - 1) {
626                 foundIndex = kEdgeIndexAtEnd;
627                 break;
628             }
629 
630             if (!isRowHidden(testIndex)) {
631                 foundIndex = testIndex;
632                 break;
633             }
634 
635             ++testIndex;
636         }
637         break; }
638     }
639 
640     cachedResult.startIndex = startIndex;
641     cachedResult.endIndex = foundIndex;
642     return foundIndex;
643 }
644 
updateContentWidth()645 void QQuickTableViewPrivate::updateContentWidth()
646 {
647     // Note that we actually never really know what the content size / size of the full table will
648     // be. Even if e.g spacing changes, and we normally would assume that the size of the table
649     // would increase accordingly, the model might also at some point have removed/hidden/resized
650     // rows/columns outside the viewport. This would also affect the size, but since we don't load
651     // rows or columns outside the viewport, this information is ignored. And even if we did, we
652     // might also have been fast-flicked to a new location at some point, and started a new rebuild
653     // there based on a new guesstimated top-left cell. So the calculated content size should always
654     // be understood as a guesstimate, which sometimes can be really off (as a tradeoff for performance).
655     // When this is not acceptable, the user can always set a custom content size explicitly.
656     Q_Q(QQuickTableView);
657 
658     if (syncHorizontally) {
659         QBoolBlocker fixupGuard(inUpdateContentSize, true);
660         q->QQuickFlickable::setContentWidth(syncView->contentWidth());
661         return;
662     }
663 
664     if (explicitContentWidth.isValid()) {
665         // Don't calculate contentWidth when it
666         // was set explicitly by the application.
667         return;
668     }
669 
670     if (loadedItems.isEmpty()) {
671         QBoolBlocker fixupGuard(inUpdateContentSize, true);
672         q->QQuickFlickable::setContentWidth(0);
673         return;
674     }
675 
676     const int nextColumn = nextVisibleEdgeIndexAroundLoadedTable(Qt::RightEdge);
677     const int columnsRemaining = nextColumn == kEdgeIndexAtEnd ? 0 : tableSize.width() - nextColumn;
678     const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
679     const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
680     const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
681     const qreal estimatedWidth = loadedTableOuterRect.right() + estimatedRemainingWidth;
682 
683     QBoolBlocker fixupGuard(inUpdateContentSize, true);
684     q->QQuickFlickable::setContentWidth(estimatedWidth);
685 }
686 
updateContentHeight()687 void QQuickTableViewPrivate::updateContentHeight()
688 {
689     Q_Q(QQuickTableView);
690 
691     if (syncVertically) {
692         QBoolBlocker fixupGuard(inUpdateContentSize, true);
693         q->QQuickFlickable::setContentHeight(syncView->contentHeight());
694         return;
695     }
696 
697     if (explicitContentHeight.isValid()) {
698         // Don't calculate contentHeight when it
699         // was set explicitly by the application.
700         return;
701     }
702 
703     if (loadedItems.isEmpty()) {
704         QBoolBlocker fixupGuard(inUpdateContentSize, true);
705         q->QQuickFlickable::setContentHeight(0);
706         return;
707     }
708 
709     const int nextRow = nextVisibleEdgeIndexAroundLoadedTable(Qt::BottomEdge);
710     const int rowsRemaining = nextRow == kEdgeIndexAtEnd ? 0 : tableSize.height() - nextRow;
711     const qreal remainingRowHeights = rowsRemaining * averageEdgeSize.height();
712     const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
713     const qreal estimatedRemainingHeight = remainingRowHeights + remainingSpacing;
714     const qreal estimatedHeight = loadedTableOuterRect.bottom() + estimatedRemainingHeight;
715 
716     QBoolBlocker fixupGuard(inUpdateContentSize, true);
717     q->QQuickFlickable::setContentHeight(estimatedHeight);
718 }
719 
updateExtents()720 void QQuickTableViewPrivate::updateExtents()
721 {
722     // When rows or columns outside the viewport are removed or added, or a rebuild
723     // forces us to guesstimate a new top-left, the edges of the table might end up
724     // out of sync with the edges of the content view. We detect this situation here, and
725     // move the origin to ensure that there will never be gaps at the end of the table.
726     // Normally we detect that the size of the whole table is not going to be equal to the
727     // size of the content view already when we load the last row/column, and especially
728     // before it's flicked completely inside the viewport. For those cases we simply adjust
729     // the origin/endExtent, to give a smooth flicking experience.
730     // But if flicking fast (e.g with a scrollbar), it can happen that the viewport ends up
731     // outside the end of the table in just one viewport update. To avoid a "blink" in the
732     // viewport when that happens, we "move" the loaded table into the viewport to cover it.
733     Q_Q(QQuickTableView);
734 
735     bool tableMovedHorizontally = false;
736     bool tableMovedVertically = false;
737 
738     const int nextLeftColumn = nextVisibleEdgeIndexAroundLoadedTable(Qt::LeftEdge);
739     const int nextRightColumn = nextVisibleEdgeIndexAroundLoadedTable(Qt::RightEdge);
740     const int nextTopRow = nextVisibleEdgeIndexAroundLoadedTable(Qt::TopEdge);
741     const int nextBottomRow = nextVisibleEdgeIndexAroundLoadedTable(Qt::BottomEdge);
742 
743     if (syncHorizontally) {
744         const auto syncView_d = syncView->d_func();
745         origin.rx() = syncView_d->origin.x();
746         endExtent.rwidth() = syncView_d->endExtent.width();
747         hData.markExtentsDirty();
748     } else if (nextLeftColumn == kEdgeIndexAtEnd) {
749         // There are no more columns to load on the left side of the table.
750         // In that case, we ensure that the origin match the beginning of the table.
751         if (loadedTableOuterRect.left() > viewportRect.left()) {
752             // We have a blank area at the left end of the viewport. In that case we don't have time to
753             // wait for the viewport to move (after changing origin), since that will take an extra
754             // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
755             // us overshooting, we brute force the loaded table inside the already existing viewport.
756             if (loadedTableOuterRect.left() > origin.x()) {
757                 const qreal diff = loadedTableOuterRect.left() - origin.x();
758                 loadedTableOuterRect.moveLeft(loadedTableOuterRect.left() - diff);
759                 loadedTableInnerRect.moveLeft(loadedTableInnerRect.left() - diff);
760                 tableMovedHorizontally = true;
761             }
762         }
763         origin.rx() = loadedTableOuterRect.left();
764         hData.markExtentsDirty();
765     } else if (loadedTableOuterRect.left() <= origin.x() + cellSpacing.width()) {
766         // The table rect is at the origin, or outside, but we still have more
767         // visible columns to the left. So we try to guesstimate how much space
768         // the rest of the columns will occupy, and move the origin accordingly.
769         const int columnsRemaining = nextLeftColumn + 1;
770         const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
771         const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
772         const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
773         origin.rx() = loadedTableOuterRect.left() - estimatedRemainingWidth;
774         hData.markExtentsDirty();
775     } else if (nextRightColumn == kEdgeIndexAtEnd) {
776         // There are no more columns to load on the right side of the table.
777         // In that case, we ensure that the end of the content view match the end of the table.
778         if (loadedTableOuterRect.right() < viewportRect.right()) {
779             // We have a blank area at the right end of the viewport. In that case we don't have time to
780             // wait for the viewport to move (after changing endExtent), since that will take an extra
781             // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
782             // us overshooting, we brute force the loaded table inside the already existing viewport.
783             const qreal w = qMin(viewportRect.right(), q->contentWidth() + endExtent.width());
784             if (loadedTableOuterRect.right() < w) {
785                 const qreal diff = loadedTableOuterRect.right() - w;
786                 loadedTableOuterRect.moveRight(loadedTableOuterRect.right() - diff);
787                 loadedTableInnerRect.moveRight(loadedTableInnerRect.right() - diff);
788                 tableMovedHorizontally = true;
789             }
790         }
791         endExtent.rwidth() = loadedTableOuterRect.right() - q->contentWidth();
792         hData.markExtentsDirty();
793     } else if (loadedTableOuterRect.right() >= q->contentWidth() + endExtent.width() - cellSpacing.width()) {
794         // The right-most column is outside the end of the content view, and we
795         // still have more visible columns in the model. This can happen if the application
796         // has set a fixed content width.
797         const int columnsRemaining = tableSize.width() - nextRightColumn;
798         const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
799         const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
800         const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
801         const qreal pixelsOutsideContentWidth = loadedTableOuterRect.right() - q->contentWidth();
802         endExtent.rwidth() = pixelsOutsideContentWidth + estimatedRemainingWidth;
803         hData.markExtentsDirty();
804     }
805 
806     if (syncVertically) {
807         const auto syncView_d = syncView->d_func();
808         origin.ry() = syncView_d->origin.y();
809         endExtent.rheight() = syncView_d->endExtent.height();
810         vData.markExtentsDirty();
811     } else if (nextTopRow == kEdgeIndexAtEnd) {
812         // There are no more rows to load on the top side of the table.
813         // In that case, we ensure that the origin match the beginning of the table.
814         if (loadedTableOuterRect.top() > viewportRect.top()) {
815             // We have a blank area at the top of the viewport. In that case we don't have time to
816             // wait for the viewport to move (after changing origin), since that will take an extra
817             // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
818             // us overshooting, we brute force the loaded table inside the already existing viewport.
819             if (loadedTableOuterRect.top() > origin.y()) {
820                 const qreal diff = loadedTableOuterRect.top() - origin.y();
821                 loadedTableOuterRect.moveTop(loadedTableOuterRect.top() - diff);
822                 loadedTableInnerRect.moveTop(loadedTableInnerRect.top() - diff);
823                 tableMovedVertically = true;
824             }
825         }
826         origin.ry() = loadedTableOuterRect.top();
827         vData.markExtentsDirty();
828     } else if (loadedTableOuterRect.top() <= origin.y() + cellSpacing.height()) {
829         // The table rect is at the origin, or outside, but we still have more
830         // visible rows at the top. So we try to guesstimate how much space
831         // the rest of the rows will occupy, and move the origin accordingly.
832         const int rowsRemaining = nextTopRow + 1;
833         const qreal remainingRowHeights = rowsRemaining * averageEdgeSize.height();
834         const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
835         const qreal estimatedRemainingHeight = remainingRowHeights + remainingSpacing;
836         origin.ry() = loadedTableOuterRect.top() - estimatedRemainingHeight;
837         vData.markExtentsDirty();
838     } else if (nextBottomRow == kEdgeIndexAtEnd) {
839         // There are no more rows to load on the bottom side of the table.
840         // In that case, we ensure that the end of the content view match the end of the table.
841         if (loadedTableOuterRect.bottom() < viewportRect.bottom()) {
842             // We have a blank area at the bottom of the viewport. In that case we don't have time to
843             // wait for the viewport to move (after changing endExtent), since that will take an extra
844             // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
845             // us overshooting, we brute force the loaded table inside the already existing viewport.
846             const qreal h = qMin(viewportRect.bottom(), q->contentHeight() + endExtent.height());
847             if (loadedTableOuterRect.bottom() < h) {
848                 const qreal diff = loadedTableOuterRect.bottom() - h;
849                 loadedTableOuterRect.moveBottom(loadedTableOuterRect.bottom() - diff);
850                 loadedTableInnerRect.moveBottom(loadedTableInnerRect.bottom() - diff);
851                 tableMovedVertically = true;
852             }
853         }
854         endExtent.rheight() = loadedTableOuterRect.bottom() - q->contentHeight();
855         vData.markExtentsDirty();
856     } else if (loadedTableOuterRect.bottom() >= q->contentHeight() + endExtent.height() - cellSpacing.height()) {
857         // The bottom-most row is outside the end of the content view, and we
858         // still have more visible rows in the model. This can happen if the application
859         // has set a fixed content height.
860         const int rowsRemaining = tableSize.height() - nextBottomRow;
861         const qreal remainingRowHeigts = rowsRemaining * averageEdgeSize.height();
862         const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
863         const qreal estimatedRemainingHeight = remainingRowHeigts + remainingSpacing;
864         const qreal pixelsOutsideContentHeight = loadedTableOuterRect.bottom() - q->contentHeight();
865         endExtent.rheight() = pixelsOutsideContentHeight + estimatedRemainingHeight;
866         vData.markExtentsDirty();
867     }
868 
869     if (tableMovedHorizontally || tableMovedVertically) {
870         qCDebug(lcTableViewDelegateLifecycle) << "move table to" << loadedTableOuterRect;
871 
872         // relayoutTableItems() will take care of moving the existing
873         // delegate items into the new loadedTableOuterRect.
874         relayoutTableItems();
875 
876         // Inform the sync children that they need to rebuild to stay in sync
877         for (auto syncChild : qAsConst(syncChildren)) {
878             auto syncChild_d = syncChild->d_func();
879             syncChild_d->scheduledRebuildOptions |= RebuildOption::ViewportOnly;
880             if (tableMovedHorizontally)
881                 syncChild_d->scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn;
882             if (tableMovedVertically)
883                 syncChild_d->scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow;
884         }
885     }
886 
887     if (hData.minExtentDirty || vData.minExtentDirty) {
888         qCDebug(lcTableViewDelegateLifecycle) << "move origin and endExtent to:" << origin << endExtent;
889         // updateBeginningEnd() will let the new extents take effect. This will also change the
890         // visualArea of the flickable, which again will cause any attached scrollbars to adjust
891         // the position of the handle. Note the latter will cause the viewport to move once more.
892         updateBeginningEnd();
893     }
894 }
895 
updateAverageColumnWidth()896 void QQuickTableViewPrivate::updateAverageColumnWidth()
897 {
898     if (explicitContentWidth.isValid()) {
899         const qreal accColumnSpacing = (tableSize.width() - 1) * cellSpacing.width();
900         averageEdgeSize.setWidth((explicitContentWidth - accColumnSpacing) / tableSize.width());
901     } else {
902         const qreal accColumnSpacing = (loadedColumns.count() - 1) * cellSpacing.width();
903         averageEdgeSize.setWidth((loadedTableOuterRect.width() - accColumnSpacing) / loadedColumns.count());
904     }
905 }
906 
updateAverageRowHeight()907 void QQuickTableViewPrivate::updateAverageRowHeight()
908 {
909     if (explicitContentHeight.isValid()) {
910         const qreal accRowSpacing = (tableSize.height() - 1) * cellSpacing.height();
911         averageEdgeSize.setHeight((explicitContentHeight - accRowSpacing) / tableSize.height());
912     } else {
913         const qreal accRowSpacing = (loadedRows.count() - 1) * cellSpacing.height();
914         averageEdgeSize.setHeight((loadedTableOuterRect.height() - accRowSpacing) / loadedRows.count());
915     }
916 }
917 
syncLoadedTableRectFromLoadedTable()918 void QQuickTableViewPrivate::syncLoadedTableRectFromLoadedTable()
919 {
920     const QPoint topLeft = QPoint(leftColumn(), topRow());
921     const QPoint bottomRight = QPoint(rightColumn(), bottomRow());
922     QRectF topLeftRect = loadedTableItem(topLeft)->geometry();
923     QRectF bottomRightRect = loadedTableItem(bottomRight)->geometry();
924     loadedTableOuterRect = QRectF(topLeftRect.topLeft(), bottomRightRect.bottomRight());
925     loadedTableInnerRect = QRectF(topLeftRect.bottomRight(), bottomRightRect.topLeft());
926 }
927 
checkForVisibilityChanges()928 QQuickTableViewPrivate::RebuildOptions QQuickTableViewPrivate::checkForVisibilityChanges()
929 {
930     // Go through all columns from first to last, find the columns that used
931     // to be hidden and not loaded, and check if they should become visible
932     // (and vice versa). If there is a change, we need to rebuild.
933     RebuildOptions rebuildOptions = RebuildOption::None;
934 
935     for (int column = leftColumn(); column <= rightColumn(); ++column) {
936         const bool wasVisibleFromBefore = loadedColumns.contains(column);
937         const bool isVisibleNow = !qFuzzyIsNull(getColumnWidth(column));
938         if (wasVisibleFromBefore == isVisibleNow)
939             continue;
940 
941         // A column changed visibility. This means that it should
942         // either be loaded or unloaded. So we need a rebuild.
943         qCDebug(lcTableViewDelegateLifecycle) << "Column" << column << "changed visibility to" << isVisibleNow;
944         rebuildOptions.setFlag(RebuildOption::ViewportOnly);
945         if (column == leftColumn()) {
946             // The first loaded column should now be hidden. This means that we
947             // need to calculate which column should now be first instead.
948             rebuildOptions.setFlag(RebuildOption::CalculateNewTopLeftColumn);
949         }
950         break;
951     }
952 
953     // Go through all rows from first to last, and do the same as above
954     for (int row = topRow(); row <= bottomRow(); ++row) {
955         const bool wasVisibleFromBefore = loadedRows.contains(row);
956         const bool isVisibleNow = !qFuzzyIsNull(getRowHeight(row));
957         if (wasVisibleFromBefore == isVisibleNow)
958             continue;
959 
960         // A row changed visibility. This means that it should
961         // either be loaded or unloaded. So we need a rebuild.
962         qCDebug(lcTableViewDelegateLifecycle) << "Row" << row << "changed visibility to" << isVisibleNow;
963         rebuildOptions.setFlag(RebuildOption::ViewportOnly);
964         if (row == topRow())
965             rebuildOptions.setFlag(RebuildOption::CalculateNewTopLeftRow);
966         break;
967     }
968 
969     return rebuildOptions;
970 }
971 
forceLayout()972 void QQuickTableViewPrivate::forceLayout()
973 {
974     if (loadedItems.isEmpty())
975         return;
976 
977     clearEdgeSizeCache();
978     RebuildOptions rebuildOptions = RebuildOption::None;
979 
980     const QSize actualTableSize = calculateTableSize();
981     if (tableSize != actualTableSize) {
982         // This can happen if the app is calling forceLayout while
983         // the model is updated, but before we're notified about it.
984         rebuildOptions = RebuildOption::All;
985     } else {
986         // Resizing a column (or row) can result in the table going from being
987         // e.g completely inside the viewport to go outside. And in the latter
988         // case, the user needs to be able to scroll the viewport, also if
989         // flags such as Flickable.StopAtBounds is in use. So we need to
990         // update contentWidth/Height to support that case.
991         rebuildOptions = RebuildOption::LayoutOnly
992                 | RebuildOption::CalculateNewContentWidth
993                 | RebuildOption::CalculateNewContentHeight
994                 | checkForVisibilityChanges();
995     }
996 
997     scheduleRebuildTable(rebuildOptions);
998 
999     auto rootView = rootSyncView();
1000     const bool updated = rootView->d_func()->updateTableRecursive();
1001     if (!updated) {
1002         qWarning() << "TableView::forceLayout(): Cannot do an immediate re-layout during an ongoing layout!";
1003         rootView->polish();
1004     }
1005 }
1006 
syncLoadedTableFromLoadRequest()1007 void QQuickTableViewPrivate::syncLoadedTableFromLoadRequest()
1008 {
1009     if (loadRequest.edge() == Qt::Edge(0)) {
1010         // No edge means we're loading the top-left item
1011         loadedColumns.insert(loadRequest.column(), 0);
1012         loadedRows.insert(loadRequest.row(), 0);
1013         return;
1014     }
1015 
1016     switch (loadRequest.edge()) {
1017     case Qt::LeftEdge:
1018     case Qt::RightEdge:
1019         loadedColumns.insert(loadRequest.column(), 0);
1020         break;
1021     case Qt::TopEdge:
1022     case Qt::BottomEdge:
1023         loadedRows.insert(loadRequest.row(), 0);
1024         break;
1025     }
1026 }
1027 
loadedTableItem(const QPoint & cell) const1028 FxTableItem *QQuickTableViewPrivate::loadedTableItem(const QPoint &cell) const
1029 {
1030     const int modelIndex = modelIndexAtCell(cell);
1031     Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell);
1032     return loadedItems.value(modelIndex);
1033 }
1034 
createFxTableItem(const QPoint & cell,QQmlIncubator::IncubationMode incubationMode)1035 FxTableItem *QQuickTableViewPrivate::createFxTableItem(const QPoint &cell, QQmlIncubator::IncubationMode incubationMode)
1036 {
1037     Q_Q(QQuickTableView);
1038 
1039     bool ownItem = false;
1040     int modelIndex = modelIndexAtCell(cell);
1041 
1042     QObject* object = model->object(modelIndex, incubationMode);
1043     if (!object) {
1044         if (model->incubationStatus(modelIndex) == QQmlIncubator::Loading) {
1045             // Item is incubating. Return nullptr for now, and let the table call this
1046             // function again once we get a callback to itemCreatedCallback().
1047             return nullptr;
1048         }
1049 
1050         qWarning() << "TableView: failed loading index:" << modelIndex;
1051         object = new QQuickItem();
1052         ownItem = true;
1053     }
1054 
1055     QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
1056     if (!item) {
1057         // The model could not provide an QQuickItem for the
1058         // given index, so we create a placeholder instead.
1059         qWarning() << "TableView: delegate is not an item:" << modelIndex;
1060         model->release(object);
1061         item = new QQuickItem();
1062         ownItem = true;
1063     } else {
1064         QQuickAnchors *anchors = QQuickItemPrivate::get(item)->_anchors;
1065         if (anchors && anchors->activeDirections())
1066             qmlWarning(item) << "TableView: detected anchors on delegate with index: " << modelIndex
1067                              << ". Use implicitWidth and implicitHeight instead.";
1068     }
1069 
1070     if (ownItem) {
1071         // Parent item is normally set early on from initItemCallback (to
1072         // allow bindings to the parent property). But if we created the item
1073         // within this function, we need to set it explicit.
1074         item->setImplicitWidth(kDefaultColumnWidth);
1075         item->setImplicitHeight(kDefaultRowHeight);
1076         item->setParentItem(q->contentItem());
1077     }
1078     Q_TABLEVIEW_ASSERT(item->parentItem() == q->contentItem(), item->parentItem());
1079 
1080     FxTableItem *fxTableItem = new FxTableItem(item, q, ownItem);
1081     fxTableItem->setVisible(false);
1082     fxTableItem->cell = cell;
1083     fxTableItem->index = modelIndex;
1084     return fxTableItem;
1085 }
1086 
loadFxTableItem(const QPoint & cell,QQmlIncubator::IncubationMode incubationMode)1087 FxTableItem *QQuickTableViewPrivate::loadFxTableItem(const QPoint &cell, QQmlIncubator::IncubationMode incubationMode)
1088 {
1089 #ifdef QT_DEBUG
1090     // Since TableView needs to work flawlessly when e.g incubating inside an async
1091     // loader, being able to override all loading to async while debugging can be helpful.
1092     static const bool forcedAsync = forcedIncubationMode == QLatin1String("async");
1093     if (forcedAsync)
1094         incubationMode = QQmlIncubator::Asynchronous;
1095 #endif
1096 
1097     // Note that even if incubation mode is asynchronous, the item might
1098     // be ready immediately since the model has a cache of items.
1099     QBoolBlocker guard(blockItemCreatedCallback);
1100     auto item = createFxTableItem(cell, incubationMode);
1101     qCDebug(lcTableViewDelegateLifecycle) << cell << "ready?" << bool(item);
1102     return item;
1103 }
1104 
releaseLoadedItems(QQmlTableInstanceModel::ReusableFlag reusableFlag)1105 void QQuickTableViewPrivate::releaseLoadedItems(QQmlTableInstanceModel::ReusableFlag reusableFlag) {
1106     // Make a copy and clear the list of items first to avoid destroyed
1107     // items being accessed during the loop (QTBUG-61294)
1108     auto const tmpList = loadedItems;
1109     loadedItems.clear();
1110     for (FxTableItem *item : tmpList)
1111         releaseItem(item, reusableFlag);
1112 }
1113 
releaseItem(FxTableItem * fxTableItem,QQmlTableInstanceModel::ReusableFlag reusableFlag)1114 void QQuickTableViewPrivate::releaseItem(FxTableItem *fxTableItem, QQmlTableInstanceModel::ReusableFlag reusableFlag)
1115 {
1116     Q_Q(QQuickTableView);
1117     // Note that fxTableItem->item might already have been destroyed, in case
1118     // the item is owned by the QML context rather than the model (e.g ObjectModel etc).
1119     auto item = fxTableItem->item;
1120 
1121     if (fxTableItem->ownItem) {
1122         Q_TABLEVIEW_ASSERT(item, fxTableItem->index);
1123         delete item;
1124     } else if (item) {
1125         auto releaseFlag = model->release(item, reusableFlag);
1126         if (releaseFlag == QQmlInstanceModel::Pooled) {
1127             fxTableItem->setVisible(false);
1128 
1129             // If the item (or a descendant) has focus, remove it, so
1130             // that the item doesn't enter with focus when it's reused.
1131             if (QQuickWindow *window = item->window()) {
1132                 const auto focusItem = qobject_cast<QQuickItem *>(window->focusObject());
1133                 if (focusItem) {
1134                     const bool hasFocus = item == focusItem || item->isAncestorOf(focusItem);
1135                     if (hasFocus) {
1136                         const auto focusChild = QQuickItemPrivate::get(q)->subFocusItem;
1137                         QQuickWindowPrivate::get(window)->clearFocusInScope(q, focusChild, Qt::OtherFocusReason);
1138                     }
1139                 }
1140             }
1141         }
1142     }
1143 
1144     delete fxTableItem;
1145 }
1146 
unloadItem(const QPoint & cell)1147 void QQuickTableViewPrivate::unloadItem(const QPoint &cell)
1148 {
1149     const int modelIndex = modelIndexAtCell(cell);
1150     Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell);
1151     releaseItem(loadedItems.take(modelIndex), reusableFlag);
1152 }
1153 
canLoadTableEdge(Qt::Edge tableEdge,const QRectF fillRect) const1154 bool QQuickTableViewPrivate::canLoadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const
1155 {
1156     switch (tableEdge) {
1157     case Qt::LeftEdge:
1158         return loadedTableOuterRect.left() > fillRect.left() + cellSpacing.width();
1159     case Qt::RightEdge:
1160         return loadedTableOuterRect.right() < fillRect.right() - cellSpacing.width();
1161     case Qt::TopEdge:
1162         return loadedTableOuterRect.top() > fillRect.top() + cellSpacing.height();
1163     case Qt::BottomEdge:
1164         return loadedTableOuterRect.bottom() < fillRect.bottom() - cellSpacing.height();
1165     }
1166 
1167     return false;
1168 }
1169 
canUnloadTableEdge(Qt::Edge tableEdge,const QRectF fillRect) const1170 bool QQuickTableViewPrivate::canUnloadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const
1171 {
1172     // Note: if there is only one row or column left, we cannot unload, since
1173     // they are needed as anchor point for further layouting.
1174     switch (tableEdge) {
1175     case Qt::LeftEdge:
1176         if (loadedColumns.count() <= 1)
1177             return false;
1178         return loadedTableInnerRect.left() <= fillRect.left();
1179     case Qt::RightEdge:
1180         if (loadedColumns.count() <= 1)
1181             return false;
1182         return loadedTableInnerRect.right() >= fillRect.right();
1183     case Qt::TopEdge:
1184         if (loadedRows.count() <= 1)
1185             return false;
1186         return loadedTableInnerRect.top() <= fillRect.top();
1187     case Qt::BottomEdge:
1188         if (loadedRows.count() <= 1)
1189             return false;
1190         return loadedTableInnerRect.bottom() >= fillRect.bottom();
1191     }
1192     Q_TABLEVIEW_UNREACHABLE(tableEdge);
1193     return false;
1194 }
1195 
nextEdgeToLoad(const QRectF rect)1196 Qt::Edge QQuickTableViewPrivate::nextEdgeToLoad(const QRectF rect)
1197 {
1198     for (Qt::Edge edge : allTableEdges) {
1199         if (!canLoadTableEdge(edge, rect))
1200             continue;
1201         const int nextIndex = nextVisibleEdgeIndexAroundLoadedTable(edge);
1202         if (nextIndex == kEdgeIndexAtEnd)
1203             continue;
1204         return edge;
1205     }
1206 
1207     return Qt::Edge(0);
1208 }
1209 
nextEdgeToUnload(const QRectF rect)1210 Qt::Edge QQuickTableViewPrivate::nextEdgeToUnload(const QRectF rect)
1211 {
1212     for (Qt::Edge edge : allTableEdges) {
1213         if (canUnloadTableEdge(edge, rect))
1214             return edge;
1215     }
1216     return Qt::Edge(0);
1217 }
1218 
cellWidth(const QPoint & cell)1219 qreal QQuickTableViewPrivate::cellWidth(const QPoint& cell)
1220 {
1221     // Using an items width directly is not an option, since we change
1222     // it during layout (which would also cause problems when recycling items).
1223     auto const cellItem = loadedTableItem(cell)->item;
1224     return cellItem->implicitWidth();
1225 }
1226 
cellHeight(const QPoint & cell)1227 qreal QQuickTableViewPrivate::cellHeight(const QPoint& cell)
1228 {
1229     // Using an items height directly is not an option, since we change
1230     // it during layout (which would also cause problems when recycling items).
1231     auto const cellItem = loadedTableItem(cell)->item;
1232     return cellItem->implicitHeight();
1233 }
1234 
sizeHintForColumn(int column)1235 qreal QQuickTableViewPrivate::sizeHintForColumn(int column)
1236 {
1237     // Find the widest cell in the column, and return its width
1238     qreal columnWidth = 0;
1239     for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1240         const int row = r.key();
1241         columnWidth = qMax(columnWidth, cellWidth(QPoint(column, row)));
1242     }
1243 
1244     return columnWidth;
1245 }
1246 
sizeHintForRow(int row)1247 qreal QQuickTableViewPrivate::sizeHintForRow(int row)
1248 {
1249     // Find the highest cell in the row, and return its height
1250     qreal rowHeight = 0;
1251     for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1252         const int column = c.key();
1253         rowHeight = qMax(rowHeight, cellHeight(QPoint(column, row)));
1254     }
1255 
1256     return rowHeight;
1257 }
1258 
updateTableSize()1259 void QQuickTableViewPrivate::updateTableSize()
1260 {
1261     // tableSize is the same as row and column count, and will always
1262     // be the same as the number of rows and columns in the model.
1263     Q_Q(QQuickTableView);
1264 
1265     const QSize prevTableSize = tableSize;
1266     tableSize = calculateTableSize();
1267 
1268     if (prevTableSize.width() != tableSize.width())
1269         emit q->columnsChanged();
1270     if (prevTableSize.height() != tableSize.height())
1271         emit q->rowsChanged();
1272 }
1273 
calculateTableSize()1274 QSize QQuickTableViewPrivate::calculateTableSize()
1275 {
1276     QSize size(0, 0);
1277     if (tableModel)
1278         size = QSize(tableModel->columns(), tableModel->rows());
1279     else if (model)
1280         size = QSize(1, model->count());
1281 
1282     return isTransposed ? size.transposed() : size;
1283 }
1284 
getColumnLayoutWidth(int column)1285 qreal QQuickTableViewPrivate::getColumnLayoutWidth(int column)
1286 {
1287     // Return the column width specified by the application, or go
1288     // through the loaded items and calculate it as a fallback. For
1289     // layouting, the width can never be zero (or negative), as this
1290     // can lead us to be stuck in an infinite loop trying to load and
1291     // fill out the empty viewport space with empty columns.
1292     const qreal explicitColumnWidth = getColumnWidth(column);
1293     if (explicitColumnWidth >= 0)
1294         return explicitColumnWidth;
1295 
1296     if (syncHorizontally) {
1297         if (syncView->d_func()->loadedColumns.contains(column))
1298             return syncView->d_func()->getColumnLayoutWidth(column);
1299     }
1300 
1301     // Iterate over the currently visible items in the column. The downside
1302     // of doing that, is that the column width will then only be based on the implicit
1303     // width of the currently loaded items (which can be different depending on which
1304     // row you're at when the column is flicked in). The upshot is that you don't have to
1305     // bother setting columnWidthProvider for small tables, or if the implicit width doesn't vary.
1306     qreal columnWidth = sizeHintForColumn(column);
1307 
1308     if (qIsNaN(columnWidth) || columnWidth <= 0) {
1309         if (!layoutWarningIssued) {
1310             layoutWarningIssued = true;
1311             qmlWarning(q_func()) << "the delegate's implicitWidth needs to be greater than zero";
1312         }
1313         columnWidth = kDefaultColumnWidth;
1314     }
1315 
1316     return columnWidth;
1317 }
1318 
getRowLayoutHeight(int row)1319 qreal QQuickTableViewPrivate::getRowLayoutHeight(int row)
1320 {
1321     // Return the row height specified by the application, or go
1322     // through the loaded items and calculate it as a fallback. For
1323     // layouting, the height can never be zero (or negative), as this
1324     // can lead us to be stuck in an infinite loop trying to load and
1325     // fill out the empty viewport space with empty rows.
1326     const qreal explicitRowHeight = getRowHeight(row);
1327     if (explicitRowHeight >= 0)
1328         return explicitRowHeight;
1329 
1330     if (syncVertically) {
1331         if (syncView->d_func()->loadedRows.contains(row))
1332             return syncView->d_func()->getRowLayoutHeight(row);
1333     }
1334 
1335     // Iterate over the currently visible items in the row. The downside
1336     // of doing that, is that the row height will then only be based on the implicit
1337     // height of the currently loaded items (which can be different depending on which
1338     // column you're at when the row is flicked in). The upshot is that you don't have to
1339     // bother setting rowHeightProvider for small tables, or if the implicit height doesn't vary.
1340     qreal rowHeight = sizeHintForRow(row);
1341 
1342     if (qIsNaN(rowHeight) || rowHeight <= 0) {
1343         if (!layoutWarningIssued) {
1344             layoutWarningIssued = true;
1345             qmlWarning(q_func()) << "the delegate's implicitHeight needs to be greater than zero";
1346         }
1347         rowHeight = kDefaultRowHeight;
1348     }
1349 
1350     return rowHeight;
1351 }
1352 
getColumnWidth(int column)1353 qreal QQuickTableViewPrivate::getColumnWidth(int column)
1354 {
1355     // Return the width of the given column, if explicitly set. Return 0 if the column
1356     // is hidden, and -1 if the width is not set (which means that the width should
1357     // instead be calculated from the implicit size of the delegate items. This function
1358     // can be overridden by e.g HeaderView to provide the column widths by other means.
1359     const int noExplicitColumnWidth = -1;
1360 
1361     if (cachedColumnWidth.startIndex == column)
1362         return cachedColumnWidth.size;
1363 
1364     if (syncHorizontally)
1365         return syncView->d_func()->getColumnWidth(column);
1366 
1367     auto cw = columnWidths.size(column);
1368     if (cw >= 0)
1369         return cw;
1370 
1371     if (columnWidthProvider.isUndefined())
1372         return noExplicitColumnWidth;
1373 
1374     qreal columnWidth = noExplicitColumnWidth;
1375 
1376     if (columnWidthProvider.isCallable()) {
1377         auto const columnAsArgument = QJSValueList() << QJSValue(column);
1378         columnWidth = columnWidthProvider.call(columnAsArgument).toNumber();
1379         if (qIsNaN(columnWidth) || columnWidth < 0)
1380             columnWidth = noExplicitColumnWidth;
1381     } else {
1382         if (!layoutWarningIssued) {
1383             layoutWarningIssued = true;
1384             qmlWarning(q_func()) << "columnWidthProvider doesn't contain a function";
1385         }
1386         columnWidth = noExplicitColumnWidth;
1387     }
1388 
1389     cachedColumnWidth.startIndex = column;
1390     cachedColumnWidth.size = columnWidth;
1391     return columnWidth;
1392 }
1393 
getRowHeight(int row)1394 qreal QQuickTableViewPrivate::getRowHeight(int row)
1395 {
1396     // Return the height of the given row, if explicitly set. Return 0 if the row
1397     // is hidden, and -1 if the height is not set (which means that the height should
1398     // instead be calculated from the implicit size of the delegate items. This function
1399     // can be overridden by e.g HeaderView to provide the row heights by other means.
1400     const int noExplicitRowHeight = -1;
1401 
1402     if (cachedRowHeight.startIndex == row)
1403         return cachedRowHeight.size;
1404 
1405     if (syncVertically)
1406         return syncView->d_func()->getRowHeight(row);
1407 
1408     auto rh = rowHeights.size(row);
1409     if (rh >= 0)
1410         return rh;
1411 
1412     if (rowHeightProvider.isUndefined())
1413         return noExplicitRowHeight;
1414 
1415     qreal rowHeight = noExplicitRowHeight;
1416 
1417     if (rowHeightProvider.isCallable()) {
1418         auto const rowAsArgument = QJSValueList() << QJSValue(row);
1419         rowHeight = rowHeightProvider.call(rowAsArgument).toNumber();
1420         if (qIsNaN(rowHeight) || rowHeight < 0)
1421             rowHeight = noExplicitRowHeight;
1422     } else {
1423         if (!layoutWarningIssued) {
1424             layoutWarningIssued = true;
1425             qmlWarning(q_func()) << "rowHeightProvider doesn't contain a function";
1426         }
1427         rowHeight = noExplicitRowHeight;
1428     }
1429 
1430     cachedRowHeight.startIndex = row;
1431     cachedRowHeight.size = rowHeight;
1432     return rowHeight;
1433 }
1434 
isColumnHidden(int column)1435 bool QQuickTableViewPrivate::isColumnHidden(int column)
1436 {
1437     // A column is hidden if the width is explicit set to zero (either by
1438     // using a columnWidthProvider, or by overriding getColumnWidth()).
1439     return qFuzzyIsNull(getColumnWidth(column));
1440 }
1441 
isRowHidden(int row)1442 bool QQuickTableViewPrivate::isRowHidden(int row)
1443 {
1444     // A row is hidden if the height is explicit set to zero (either by
1445     // using a rowHeightProvider, or by overriding getRowHeight()).
1446     return qFuzzyIsNull(getRowHeight(row));
1447 }
1448 
relayoutTableItems()1449 void QQuickTableViewPrivate::relayoutTableItems()
1450 {
1451     qCDebug(lcTableViewDelegateLifecycle);
1452 
1453     qreal nextColumnX = loadedTableOuterRect.x();
1454     qreal nextRowY = loadedTableOuterRect.y();
1455 
1456     for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1457         const int column = c.key();
1458         // Adjust the geometry of all cells in the current column
1459         const qreal width = getColumnLayoutWidth(column);
1460 
1461         for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1462             const int row = r.key();
1463             auto item = loadedTableItem(QPoint(column, row));
1464             QRectF geometry = item->geometry();
1465             geometry.moveLeft(nextColumnX);
1466             geometry.setWidth(width);
1467             item->setGeometry(geometry);
1468         }
1469 
1470         if (width > 0)
1471             nextColumnX += width + cellSpacing.width();
1472     }
1473 
1474     for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1475         const int row = r.key();
1476         // Adjust the geometry of all cells in the current row
1477         const qreal height = getRowLayoutHeight(row);
1478 
1479         for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1480             const int column = c.key();
1481             auto item = loadedTableItem(QPoint(column, row));
1482             QRectF geometry = item->geometry();
1483             geometry.moveTop(nextRowY);
1484             geometry.setHeight(height);
1485             item->setGeometry(geometry);
1486         }
1487 
1488         if (height > 0)
1489             nextRowY += height + cellSpacing.height();
1490     }
1491 
1492     if (Q_UNLIKELY(lcTableViewDelegateLifecycle().isDebugEnabled())) {
1493         for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1494             const int column = c.key();
1495             for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1496                 const int row = r.key();
1497                 QPoint cell = QPoint(column, row);
1498                 qCDebug(lcTableViewDelegateLifecycle()) << "relayout item:" << cell << loadedTableItem(cell)->geometry();
1499             }
1500         }
1501     }
1502 }
1503 
layoutVerticalEdge(Qt::Edge tableEdge)1504 void QQuickTableViewPrivate::layoutVerticalEdge(Qt::Edge tableEdge)
1505 {
1506     int columnThatNeedsLayout;
1507     int neighbourColumn;
1508     qreal columnX;
1509     qreal columnWidth;
1510 
1511     if (tableEdge == Qt::LeftEdge) {
1512         columnThatNeedsLayout = leftColumn();
1513         neighbourColumn = loadedColumns.keys().value(1);
1514         columnWidth = getColumnLayoutWidth(columnThatNeedsLayout);
1515         const auto neighbourItem = loadedTableItem(QPoint(neighbourColumn, topRow()));
1516         columnX = neighbourItem->geometry().left() - cellSpacing.width() - columnWidth;
1517     } else {
1518         columnThatNeedsLayout = rightColumn();
1519         neighbourColumn = loadedColumns.keys().value(loadedColumns.count() - 2);
1520         columnWidth = getColumnLayoutWidth(columnThatNeedsLayout);
1521         const auto neighbourItem = loadedTableItem(QPoint(neighbourColumn, topRow()));
1522         columnX = neighbourItem->geometry().right() + cellSpacing.width();
1523     }
1524 
1525     for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1526         const int row = r.key();
1527         auto fxTableItem = loadedTableItem(QPoint(columnThatNeedsLayout, row));
1528         auto const neighbourItem = loadedTableItem(QPoint(neighbourColumn, row));
1529         const qreal rowY = neighbourItem->geometry().y();
1530         const qreal rowHeight = neighbourItem->geometry().height();
1531 
1532         fxTableItem->setGeometry(QRectF(columnX, rowY, columnWidth, rowHeight));
1533         fxTableItem->setVisible(true);
1534 
1535         qCDebug(lcTableViewDelegateLifecycle()) << "layout item:" << QPoint(columnThatNeedsLayout, row) << fxTableItem->geometry();
1536     }
1537 }
1538 
layoutHorizontalEdge(Qt::Edge tableEdge)1539 void QQuickTableViewPrivate::layoutHorizontalEdge(Qt::Edge tableEdge)
1540 {
1541     int rowThatNeedsLayout;
1542     int neighbourRow;
1543 
1544     if (tableEdge == Qt::TopEdge) {
1545         rowThatNeedsLayout = topRow();
1546         neighbourRow = loadedRows.keys().value(1);
1547     } else {
1548         rowThatNeedsLayout = bottomRow();
1549         neighbourRow = loadedRows.keys().value(loadedRows.count() - 2);
1550     }
1551 
1552     // Set the width first, since text items in QtQuick will calculate
1553     // implicitHeight based on the text items width.
1554     for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1555         const int column = c.key();
1556         auto fxTableItem = loadedTableItem(QPoint(column, rowThatNeedsLayout));
1557         auto const neighbourItem = loadedTableItem(QPoint(column, neighbourRow));
1558         const qreal columnX = neighbourItem->geometry().x();
1559         const qreal columnWidth = neighbourItem->geometry().width();
1560         fxTableItem->item->setX(columnX);
1561         fxTableItem->item->setWidth(columnWidth);
1562     }
1563 
1564     qreal rowY;
1565     qreal rowHeight;
1566     if (tableEdge == Qt::TopEdge) {
1567         rowHeight = getRowLayoutHeight(rowThatNeedsLayout);
1568         const auto neighbourItem = loadedTableItem(QPoint(leftColumn(), neighbourRow));
1569         rowY = neighbourItem->geometry().top() - cellSpacing.height() - rowHeight;
1570     } else {
1571         rowHeight = getRowLayoutHeight(rowThatNeedsLayout);
1572         const auto neighbourItem = loadedTableItem(QPoint(leftColumn(), neighbourRow));
1573         rowY = neighbourItem->geometry().bottom() + cellSpacing.height();
1574     }
1575 
1576     for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1577         const int column = c.key();
1578         auto fxTableItem = loadedTableItem(QPoint(column, rowThatNeedsLayout));
1579         fxTableItem->item->setY(rowY);
1580         fxTableItem->item->setHeight(rowHeight);
1581         fxTableItem->setVisible(true);
1582 
1583         qCDebug(lcTableViewDelegateLifecycle()) << "layout item:" << QPoint(column, rowThatNeedsLayout) << fxTableItem->geometry();
1584     }
1585 }
1586 
layoutTopLeftItem()1587 void QQuickTableViewPrivate::layoutTopLeftItem()
1588 {
1589     const QPoint cell(loadRequest.column(), loadRequest.row());
1590     auto topLeftItem = loadedTableItem(cell);
1591     auto item = topLeftItem->item;
1592 
1593     item->setPosition(loadRequest.startPosition());
1594     item->setSize(QSizeF(getColumnLayoutWidth(cell.x()), getRowLayoutHeight(cell.y())));
1595     topLeftItem->setVisible(true);
1596     qCDebug(lcTableViewDelegateLifecycle) << "geometry:" << topLeftItem->geometry();
1597 }
1598 
layoutTableEdgeFromLoadRequest()1599 void QQuickTableViewPrivate::layoutTableEdgeFromLoadRequest()
1600 {
1601     if (loadRequest.edge() == Qt::Edge(0)) {
1602         // No edge means we're loading the top-left item
1603         layoutTopLeftItem();
1604         return;
1605     }
1606 
1607     switch (loadRequest.edge()) {
1608     case Qt::LeftEdge:
1609     case Qt::RightEdge:
1610         layoutVerticalEdge(loadRequest.edge());
1611         break;
1612     case Qt::TopEdge:
1613     case Qt::BottomEdge:
1614         layoutHorizontalEdge(loadRequest.edge());
1615         break;
1616     }
1617 }
1618 
processLoadRequest()1619 void QQuickTableViewPrivate::processLoadRequest()
1620 {
1621     Q_TABLEVIEW_ASSERT(loadRequest.isActive(), "");
1622 
1623     while (loadRequest.hasCurrentCell()) {
1624         QPoint cell = loadRequest.currentCell();
1625         FxTableItem *fxTableItem = loadFxTableItem(cell, loadRequest.incubationMode());
1626 
1627         if (!fxTableItem) {
1628             // Requested item is not yet ready. Just leave, and wait for this
1629             // function to be called again when the item is ready.
1630             return;
1631         }
1632 
1633         loadedItems.insert(modelIndexAtCell(cell), fxTableItem);
1634         loadRequest.moveToNextCell();
1635     }
1636 
1637     qCDebug(lcTableViewDelegateLifecycle()) << "all items loaded!";
1638 
1639     syncLoadedTableFromLoadRequest();
1640     layoutTableEdgeFromLoadRequest();
1641     syncLoadedTableRectFromLoadedTable();
1642 
1643     if (rebuildState == RebuildState::Done) {
1644         // Loading of this edge was not done as a part of a rebuild, but
1645         // instead as an incremental build after e.g a flick.
1646         updateExtents();
1647         drainReusePoolAfterLoadRequest();
1648     }
1649 
1650     loadRequest.markAsDone();
1651 
1652     qCDebug(lcTableViewDelegateLifecycle()) << "request completed! Table:" << tableLayoutToString();
1653 }
1654 
processRebuildTable()1655 void QQuickTableViewPrivate::processRebuildTable()
1656 {
1657     Q_Q(QQuickTableView);
1658 
1659     if (rebuildState == RebuildState::Begin) {
1660         if (Q_UNLIKELY(lcTableViewDelegateLifecycle().isDebugEnabled())) {
1661             qCDebug(lcTableViewDelegateLifecycle()) << "begin rebuild:" << q;
1662             if (rebuildOptions & RebuildOption::All)
1663                 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::All, options:" << rebuildOptions;
1664             else if (rebuildOptions & RebuildOption::ViewportOnly)
1665                 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::ViewportOnly, options:" << rebuildOptions;
1666             else if (rebuildOptions & RebuildOption::LayoutOnly)
1667                 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::LayoutOnly, options:" << rebuildOptions;
1668             else
1669                 Q_TABLEVIEW_UNREACHABLE(rebuildOptions);
1670         }
1671     }
1672 
1673     moveToNextRebuildState();
1674 
1675     if (rebuildState == RebuildState::LoadInitalTable) {
1676         beginRebuildTable();
1677         if (!moveToNextRebuildState())
1678             return;
1679     }
1680 
1681     if (rebuildState == RebuildState::VerifyTable) {
1682         if (loadedItems.isEmpty()) {
1683             qCDebug(lcTableViewDelegateLifecycle()) << "no items loaded!";
1684             updateContentWidth();
1685             updateContentHeight();
1686             rebuildState = RebuildState::Done;
1687         } else if (!moveToNextRebuildState()) {
1688             return;
1689         }
1690     }
1691 
1692     if (rebuildState == RebuildState::LayoutTable) {
1693         layoutAfterLoadingInitialTable();
1694         if (!moveToNextRebuildState())
1695             return;
1696     }
1697 
1698     if (rebuildState == RebuildState::LoadAndUnloadAfterLayout) {
1699         loadAndUnloadVisibleEdges();
1700         if (!moveToNextRebuildState())
1701             return;
1702     }
1703 
1704     const bool preload = (rebuildOptions & RebuildOption::All
1705                           && reusableFlag == QQmlTableInstanceModel::Reusable);
1706 
1707     if (rebuildState == RebuildState::PreloadColumns) {
1708         if (preload && nextVisibleEdgeIndexAroundLoadedTable(Qt::RightEdge) != kEdgeIndexAtEnd)
1709             loadEdge(Qt::RightEdge, QQmlIncubator::AsynchronousIfNested);
1710         if (!moveToNextRebuildState())
1711             return;
1712     }
1713 
1714     if (rebuildState == RebuildState::PreloadRows) {
1715         if (preload && nextVisibleEdgeIndexAroundLoadedTable(Qt::BottomEdge) != kEdgeIndexAtEnd)
1716             loadEdge(Qt::BottomEdge, QQmlIncubator::AsynchronousIfNested);
1717         if (!moveToNextRebuildState())
1718             return;
1719     }
1720 
1721     if (rebuildState == RebuildState::MovePreloadedItemsToPool) {
1722         while (Qt::Edge edge = nextEdgeToUnload(viewportRect))
1723             unloadEdge(edge);
1724         if (!moveToNextRebuildState())
1725             return;
1726     }
1727 
1728     Q_TABLEVIEW_ASSERT(rebuildState == RebuildState::Done, int(rebuildState));
1729     qCDebug(lcTableViewDelegateLifecycle()) << "rebuild complete:" << q;
1730 }
1731 
moveToNextRebuildState()1732 bool QQuickTableViewPrivate::moveToNextRebuildState()
1733 {
1734     if (loadRequest.isActive()) {
1735         // Items are still loading async, which means
1736         // that the current state is not yet done.
1737         return false;
1738     }
1739 
1740     if (rebuildState == RebuildState::Begin
1741             && rebuildOptions.testFlag(RebuildOption::LayoutOnly))
1742         rebuildState = RebuildState::LayoutTable;
1743     else
1744         rebuildState = RebuildState(int(rebuildState) + 1);
1745 
1746     qCDebug(lcTableViewDelegateLifecycle()) << int(rebuildState);
1747     return true;
1748 }
1749 
calculateTopLeft(QPoint & topLeftCell,QPointF & topLeftPos)1750 void QQuickTableViewPrivate::calculateTopLeft(QPoint &topLeftCell, QPointF &topLeftPos)
1751 {
1752     if (tableSize.isEmpty()) {
1753         // There is no cell that can be top left
1754         topLeftCell.rx() = kEdgeIndexAtEnd;
1755         topLeftCell.ry() = kEdgeIndexAtEnd;
1756         return;
1757     }
1758 
1759     if (syncHorizontally || syncVertically) {
1760         const auto syncView_d = syncView->d_func();
1761 
1762         if (syncView_d->loadedItems.isEmpty()) {
1763             topLeftCell.rx() = 0;
1764             topLeftCell.ry() = 0;
1765             return;
1766         }
1767 
1768         // Get sync view top left, and use that as our own top left (if possible)
1769         const QPoint syncViewTopLeftCell(syncView_d->leftColumn(), syncView_d->topRow());
1770         const auto syncViewTopLeftFxItem = syncView_d->loadedTableItem(syncViewTopLeftCell);
1771         const QPointF syncViewTopLeftPos = syncViewTopLeftFxItem->geometry().topLeft();
1772 
1773         if (syncHorizontally) {
1774             topLeftCell.rx() = syncViewTopLeftCell.x();
1775             topLeftPos.rx() = syncViewTopLeftPos.x();
1776 
1777             if (topLeftCell.x() >= tableSize.width()) {
1778                 // Top left is outside our own model.
1779                 topLeftCell.rx() = kEdgeIndexAtEnd;
1780                 topLeftPos.rx() = kEdgeIndexAtEnd;
1781             }
1782         }
1783 
1784         if (syncVertically) {
1785             topLeftCell.ry() = syncViewTopLeftCell.y();
1786             topLeftPos.ry() = syncViewTopLeftPos.y();
1787 
1788             if (topLeftCell.y() >= tableSize.height()) {
1789                 // Top left is outside our own model.
1790                 topLeftCell.ry() = kEdgeIndexAtEnd;
1791                 topLeftPos.ry() = kEdgeIndexAtEnd;
1792             }
1793         }
1794 
1795         if (syncHorizontally && syncVertically) {
1796             // We have a valid top left, so we're done
1797             return;
1798         }
1799     }
1800 
1801     // Since we're not sync-ing both horizontal and vertical, calculate the missing
1802     // dimention(s) ourself. If we rebuild all, we find the first visible top-left
1803     // item starting from cell(0, 0). Otherwise, guesstimate which row or column that
1804     // should be the new top-left given the geometry of the viewport.
1805 
1806     if (!syncHorizontally) {
1807         if (rebuildOptions & RebuildOption::All) {
1808             // Find the first visible column from the beginning
1809             topLeftCell.rx() = nextVisibleEdgeIndex(Qt::RightEdge, 0);
1810             if (topLeftCell.x() == kEdgeIndexAtEnd) {
1811                 // No visible column found
1812                 return;
1813             }
1814         } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftColumn) {
1815             // Guesstimate new top left
1816             const int newColumn = int(viewportRect.x() / (averageEdgeSize.width() + cellSpacing.width()));
1817             topLeftCell.rx() = qBound(0, newColumn, tableSize.width() - 1);
1818             topLeftPos.rx() = topLeftCell.x() * (averageEdgeSize.width() + cellSpacing.width());
1819         } else {
1820             // Keep the current top left, unless it's outside model
1821             topLeftCell.rx() = qBound(0, leftColumn(), tableSize.width() - 1);
1822             topLeftPos.rx() = loadedTableOuterRect.topLeft().x();
1823         }
1824     }
1825 
1826     if (!syncVertically) {
1827         if (rebuildOptions & RebuildOption::All) {
1828             // Find the first visible row from the beginning
1829             topLeftCell.ry() = nextVisibleEdgeIndex(Qt::BottomEdge, 0);
1830             if (topLeftCell.y() == kEdgeIndexAtEnd) {
1831                 // No visible row found
1832                 return;
1833             }
1834         } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftRow) {
1835             // Guesstimate new top left
1836             const int newRow = int(viewportRect.y() / (averageEdgeSize.height() + cellSpacing.height()));
1837             topLeftCell.ry() = qBound(0, newRow, tableSize.height() - 1);
1838             topLeftPos.ry() = topLeftCell.y() * (averageEdgeSize.height() + cellSpacing.height());
1839         } else {
1840             // Keep the current top left, unless it's outside model
1841             topLeftCell.ry() = qBound(0, topRow(), tableSize.height() - 1);
1842             topLeftPos.ry() = loadedTableOuterRect.topLeft().y();
1843         }
1844     }
1845 }
1846 
beginRebuildTable()1847 void QQuickTableViewPrivate::beginRebuildTable()
1848 {
1849     updateTableSize();
1850 
1851     QPoint topLeft;
1852     QPointF topLeftPos;
1853     calculateTopLeft(topLeft, topLeftPos);
1854 
1855     if (!loadedItems.isEmpty()) {
1856         if (rebuildOptions & RebuildOption::All)
1857             releaseLoadedItems(QQmlTableInstanceModel::NotReusable);
1858         else if (rebuildOptions & RebuildOption::ViewportOnly)
1859             releaseLoadedItems(reusableFlag);
1860     }
1861 
1862     if (rebuildOptions & RebuildOption::All) {
1863         origin = QPointF(0, 0);
1864         endExtent = QSizeF(0, 0);
1865         hData.markExtentsDirty();
1866         vData.markExtentsDirty();
1867         updateBeginningEnd();
1868     }
1869 
1870     loadedColumns.clear();
1871     loadedRows.clear();
1872     loadedTableOuterRect = QRect();
1873     loadedTableInnerRect = QRect();
1874     clearEdgeSizeCache();
1875 
1876     if (syncHorizontally) {
1877         setLocalViewportX(syncView->contentX());
1878         viewportRect.moveLeft(syncView->d_func()->viewportRect.left());
1879     }
1880 
1881     if (syncVertically) {
1882         setLocalViewportY(syncView->contentY());
1883         viewportRect.moveTop(syncView->d_func()->viewportRect.top());
1884     }
1885 
1886     syncViewportRect();
1887 
1888     if (!model) {
1889         qCDebug(lcTableViewDelegateLifecycle()) << "no model found, leaving table empty";
1890         return;
1891     }
1892 
1893     if (model->count() == 0) {
1894         qCDebug(lcTableViewDelegateLifecycle()) << "empty model found, leaving table empty";
1895         return;
1896     }
1897 
1898     if (tableModel && !tableModel->delegate()) {
1899         qCDebug(lcTableViewDelegateLifecycle()) << "no delegate found, leaving table empty";
1900         return;
1901     }
1902 
1903     if (topLeft.x() == kEdgeIndexAtEnd || topLeft.y() == kEdgeIndexAtEnd) {
1904         qCDebug(lcTableViewDelegateLifecycle()) << "no visible row or column found, leaving table empty";
1905         return;
1906     }
1907 
1908     if (topLeft.x() == kEdgeIndexNotSet || topLeft.y() == kEdgeIndexNotSet) {
1909         qCDebug(lcTableViewDelegateLifecycle()) << "could not resolve top-left item, leaving table empty";
1910         return;
1911     }
1912 
1913     // Load top-left item. After loaded, loadItemsInsideRect() will take
1914     // care of filling out the rest of the table.
1915     loadRequest.begin(topLeft, topLeftPos, QQmlIncubator::AsynchronousIfNested);
1916     processLoadRequest();
1917     loadAndUnloadVisibleEdges();
1918 }
1919 
layoutAfterLoadingInitialTable()1920 void QQuickTableViewPrivate::layoutAfterLoadingInitialTable()
1921 {
1922     clearEdgeSizeCache();
1923     relayoutTableItems();
1924     syncLoadedTableRectFromLoadedTable();
1925 
1926     if (rebuildOptions.testFlag(RebuildOption::CalculateNewContentWidth)) {
1927         updateAverageColumnWidth();
1928         updateContentWidth();
1929     }
1930 
1931     if (rebuildOptions.testFlag(RebuildOption::CalculateNewContentHeight)) {
1932         updateAverageRowHeight();
1933         updateContentHeight();
1934     }
1935 
1936     updateExtents();
1937 }
1938 
unloadEdge(Qt::Edge edge)1939 void QQuickTableViewPrivate::unloadEdge(Qt::Edge edge)
1940 {
1941     qCDebug(lcTableViewDelegateLifecycle) << edge;
1942 
1943     switch (edge) {
1944     case Qt::LeftEdge:
1945     case Qt::RightEdge: {
1946         const int column = edge == Qt::LeftEdge ? leftColumn() : rightColumn();
1947         for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r)
1948             unloadItem(QPoint(column, r.key()));
1949         loadedColumns.remove(column);
1950         syncLoadedTableRectFromLoadedTable();
1951         break; }
1952     case Qt::TopEdge:
1953     case Qt::BottomEdge: {
1954         const int row = edge == Qt::TopEdge ? topRow() : bottomRow();
1955         for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c)
1956             unloadItem(QPoint(c.key(), row));
1957         loadedRows.remove(row);
1958         syncLoadedTableRectFromLoadedTable();
1959         break; }
1960     }
1961 
1962     qCDebug(lcTableViewDelegateLifecycle) << tableLayoutToString();
1963 }
1964 
loadEdge(Qt::Edge edge,QQmlIncubator::IncubationMode incubationMode)1965 void QQuickTableViewPrivate::loadEdge(Qt::Edge edge, QQmlIncubator::IncubationMode incubationMode)
1966 {
1967     const int edgeIndex = nextVisibleEdgeIndexAroundLoadedTable(edge);
1968     qCDebug(lcTableViewDelegateLifecycle) << edge << edgeIndex;
1969 
1970     const QList<int> visibleCells = edge & (Qt::LeftEdge | Qt::RightEdge)
1971             ? loadedRows.keys() : loadedColumns.keys();
1972     loadRequest.begin(edge, edgeIndex, visibleCells, incubationMode);
1973     processLoadRequest();
1974 }
1975 
loadAndUnloadVisibleEdges()1976 void QQuickTableViewPrivate::loadAndUnloadVisibleEdges()
1977 {
1978     // Unload table edges that have been moved outside the visible part of the
1979     // table (including buffer area), and load new edges that has been moved inside.
1980     // Note: an important point is that we always keep the table rectangular
1981     // and without holes to reduce complexity (we never leave the table in
1982     // a half-loaded state, or keep track of multiple patches).
1983     // We load only one edge (row or column) at a time. This is especially
1984     // important when loading into the buffer, since we need to be able to
1985     // cancel the buffering quickly if the user starts to flick, and then
1986     // focus all further loading on the edges that are flicked into view.
1987 
1988     if (loadRequest.isActive()) {
1989         // Don't start loading more edges while we're
1990         // already waiting for another one to load.
1991         return;
1992     }
1993 
1994     if (loadedItems.isEmpty()) {
1995         // We need at least the top-left item to be loaded before we can
1996         // start loading edges around it. Not having a top-left item at
1997         // this point means that the model is empty (or no delegate).
1998         return;
1999     }
2000 
2001     bool tableModified;
2002 
2003     do {
2004         tableModified = false;
2005 
2006         if (Qt::Edge edge = nextEdgeToUnload(viewportRect)) {
2007             tableModified = true;
2008             unloadEdge(edge);
2009         }
2010 
2011         if (Qt::Edge edge = nextEdgeToLoad(viewportRect)) {
2012             tableModified = true;
2013             loadEdge(edge, QQmlIncubator::AsynchronousIfNested);
2014             if (loadRequest.isActive())
2015                 return;
2016         }
2017     } while (tableModified);
2018 
2019 }
2020 
drainReusePoolAfterLoadRequest()2021 void QQuickTableViewPrivate::drainReusePoolAfterLoadRequest()
2022 {
2023     Q_Q(QQuickTableView);
2024 
2025     if (reusableFlag == QQmlTableInstanceModel::NotReusable || !tableModel)
2026         return;
2027 
2028     if (!qFuzzyIsNull(q->verticalOvershoot()) || !qFuzzyIsNull(q->horizontalOvershoot())) {
2029         // Don't drain while we're overshooting, since this will fill up the
2030         // pool, but we expect to reuse them all once the content item moves back.
2031         return;
2032     }
2033 
2034     // When loading edges, we don't want to drain the reuse pool too aggressively. Normally,
2035     // all the items in the pool are reused rapidly as the content view is flicked around
2036     // anyway. Even if the table is temporarily flicked to a section that contains fewer
2037     // cells than what used to be (e.g if the flicked-in rows are taller than average), it
2038     // still makes sense to keep all the items in circulation; Chances are, that soon enough,
2039     // thinner rows are flicked back in again (meaning that we can fit more items into the
2040     // view). But at the same time, if a delegate chooser is in use, the pool might contain
2041     // items created from different delegates. And some of those delegates might be used only
2042     // occasionally. So to avoid situations where an item ends up in the pool for too long, we
2043     // call drain after each load request, but with a sufficiently large pool time. (If an item
2044     // in the pool has a large pool time, it means that it hasn't been reused for an equal
2045     // amount of load cycles, and should be released).
2046     //
2047     // We calculate an appropriate pool time by figuring out what the minimum time must be to
2048     // not disturb frequently reused items. Since the number of items in a row might be higher
2049     // than in a column (or vice versa), the minimum pool time should take into account that
2050     // you might be flicking out a single row (filling up the pool), before you continue
2051     // flicking in several new columns (taking them out again, but now in smaller chunks). This
2052     // will increase the number of load cycles items are kept in the pool (poolTime), but still,
2053     // we shouldn't release them, as they are still being reused frequently.
2054     // To get a flexible maxValue (that e.g tolerates rows and columns being flicked
2055     // in with varying sizes, causing some items not to be resued immediately), we multiply the
2056     // value by 2. Note that we also add an extra +1 to the column count, because the number of
2057     // visible columns will fluctuate between +1/-1 while flicking.
2058     const int w = loadedColumns.count();
2059     const int h = loadedRows.count();
2060     const int minTime = int(std::ceil(w > h ? qreal(w + 1) / h : qreal(h + 1) / w));
2061     const int maxTime = minTime * 2;
2062     tableModel->drainReusableItemsPool(maxTime);
2063 }
2064 
scheduleRebuildTable(RebuildOptions options)2065 void QQuickTableViewPrivate::scheduleRebuildTable(RebuildOptions options) {
2066     if (!q_func()->isComponentComplete()) {
2067         // We'll rebuild the table once complete anyway
2068         return;
2069     }
2070 
2071     scheduledRebuildOptions |= options;
2072     q_func()->polish();
2073 }
2074 
rootSyncView() const2075 QQuickTableView *QQuickTableViewPrivate::rootSyncView() const
2076 {
2077     QQuickTableView *root = const_cast<QQuickTableView *>(q_func());
2078     while (QQuickTableView *view = root->d_func()->syncView)
2079         root = view;
2080     return root;
2081 }
2082 
updatePolish()2083 void QQuickTableViewPrivate::updatePolish()
2084 {
2085     // We always start updating from the top of the syncView tree, since
2086     // the layout of a syncView child will depend on the layout of the syncView.
2087     //  E.g when a new column is flicked in, the syncView should load and layout
2088     // the column first, before any syncChildren gets a chance to do the same.
2089     Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!");
2090     rootSyncView()->d_func()->updateTableRecursive();
2091 }
2092 
updateTableRecursive()2093 bool QQuickTableViewPrivate::updateTableRecursive()
2094 {
2095     if (polishing) {
2096         // We're already updating the Table in this view, so
2097         // we cannot continue. Signal this back by returning false.
2098         // The caller can then choose to call "polish()" instead, to
2099         // do the update later.
2100         return false;
2101     }
2102 
2103     const bool updateComplete = updateTable();
2104     if (!updateComplete)
2105         return false;
2106 
2107     for (auto syncChild : qAsConst(syncChildren)) {
2108         auto syncChild_d = syncChild->d_func();
2109         syncChild_d->scheduledRebuildOptions |= rebuildOptions;
2110 
2111         const bool descendantUpdateComplete = syncChild_d->updateTableRecursive();
2112         if (!descendantUpdateComplete)
2113             return false;
2114     }
2115 
2116     rebuildOptions = RebuildOption::None;
2117 
2118     return true;
2119 }
2120 
updateTable()2121 bool QQuickTableViewPrivate::updateTable()
2122 {
2123     // Whenever something changes, e.g viewport moves, spacing is set to a
2124     // new value, model changes etc, this function will end up being called. Here
2125     // we check what needs to be done, and load/unload cells accordingly.
2126     // If we cannot complete the update (because we need to wait for an item
2127     // to load async), we return false.
2128 
2129     Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!");
2130     QBoolBlocker polishGuard(polishing, true);
2131 
2132     if (loadRequest.isActive()) {
2133         // We're currently loading items async to build a new edge in the table. We see the loading
2134         // as an atomic operation, which means that we don't continue doing anything else until all
2135         // items have been received and laid out. Note that updatePolish is then called once more
2136         // after the loadRequest has completed to handle anything that might have occurred in-between.
2137         return false;
2138     }
2139 
2140     if (rebuildState != RebuildState::Done) {
2141         processRebuildTable();
2142         return rebuildState == RebuildState::Done;
2143     }
2144 
2145     syncWithPendingChanges();
2146 
2147     if (rebuildState == RebuildState::Begin) {
2148         processRebuildTable();
2149         return rebuildState == RebuildState::Done;
2150     }
2151 
2152     if (loadedItems.isEmpty())
2153         return !loadRequest.isActive();
2154 
2155     loadAndUnloadVisibleEdges();
2156 
2157     return !loadRequest.isActive();
2158 }
2159 
fixup(QQuickFlickablePrivate::AxisData & data,qreal minExtent,qreal maxExtent)2160 void QQuickTableViewPrivate::fixup(QQuickFlickablePrivate::AxisData &data, qreal minExtent, qreal maxExtent)
2161 {
2162     if (inUpdateContentSize) {
2163         // We update the content size dynamically as we load and unload edges.
2164         // Unfortunately, this also triggers a call to this function. The base
2165         // implementation will do things like start a momentum animation or move
2166         // the content view somewhere else, which causes glitches. This can
2167         // especially happen if flicking on one of the syncView children, which triggers
2168         // an update to our content size. In that case, the base implementation don't know
2169         // that the view is being indirectly dragged, and will therefore do strange things as
2170         // it tries to 'fixup' the geometry. So we use a guard to prevent this from happening.
2171         return;
2172     }
2173 
2174     QQuickFlickablePrivate::fixup(data, minExtent, maxExtent);
2175 }
2176 
resolveImportVersion()2177 int QQuickTableViewPrivate::resolveImportVersion()
2178 {
2179     const auto data = QQmlData::get(q_func());
2180     if (!data || !data->propertyCache)
2181         return 0;
2182 
2183     const auto cppMetaObject = data->propertyCache->firstCppMetaObject();
2184     const auto qmlTypeView = QQmlMetaType::qmlType(cppMetaObject);
2185     return qmlTypeView.minorVersion();
2186 }
2187 
createWrapperModel()2188 void QQuickTableViewPrivate::createWrapperModel()
2189 {
2190     Q_Q(QQuickTableView);
2191     // When the assigned model is not an instance model, we create a wrapper
2192     // model (QQmlTableInstanceModel) that keeps a pointer to both the
2193     // assigned model and the assigned delegate. This model will give us a
2194     // common interface to any kind of model (js arrays, QAIM, number etc), and
2195     // help us create delegate instances.
2196     tableModel = new QQmlTableInstanceModel(qmlContext(q));
2197     tableModel->useImportVersion(resolveImportVersion());
2198     model = tableModel;
2199 }
2200 
itemCreatedCallback(int modelIndex,QObject *)2201 void QQuickTableViewPrivate::itemCreatedCallback(int modelIndex, QObject*)
2202 {
2203     if (blockItemCreatedCallback)
2204         return;
2205 
2206     qCDebug(lcTableViewDelegateLifecycle) << "item done loading:"
2207         << cellAtModelIndex(modelIndex);
2208 
2209     // Since the item we waited for has finished incubating, we can
2210     // continue with the load request. processLoadRequest will
2211     // ask the model for the requested item once more, which will be
2212     // quick since the model has cached it.
2213     processLoadRequest();
2214     loadAndUnloadVisibleEdges();
2215     updatePolish();
2216 }
2217 
initItemCallback(int modelIndex,QObject * object)2218 void QQuickTableViewPrivate::initItemCallback(int modelIndex, QObject *object)
2219 {
2220     Q_UNUSED(modelIndex);
2221     Q_Q(QQuickTableView);
2222 
2223     if (auto item = qmlobject_cast<QQuickItem*>(object)) {
2224         item->setParentItem(q->contentItem());
2225         item->setZ(1);
2226     }
2227 
2228     if (auto attached = getAttachedObject(object))
2229         attached->setView(q);
2230 }
2231 
itemPooledCallback(int modelIndex,QObject * object)2232 void QQuickTableViewPrivate::itemPooledCallback(int modelIndex, QObject *object)
2233 {
2234     Q_UNUSED(modelIndex);
2235 
2236     if (auto attached = getAttachedObject(object))
2237         emit attached->pooled();
2238 }
2239 
itemReusedCallback(int modelIndex,QObject * object)2240 void QQuickTableViewPrivate::itemReusedCallback(int modelIndex, QObject *object)
2241 {
2242     Q_UNUSED(modelIndex);
2243 
2244     if (auto attached = getAttachedObject(object))
2245         emit attached->reused();
2246 }
2247 
syncWithPendingChanges()2248 void QQuickTableViewPrivate::syncWithPendingChanges()
2249 {
2250     // The application can change properties like the model or the delegate while
2251     // we're e.g in the middle of e.g loading a new row. Since this will lead to
2252     // unpredicted behavior, and possibly a crash, we need to postpone taking
2253     // such assignments into effect until we're in a state that allows it.
2254 
2255     syncViewportRect();
2256     syncModel();
2257     syncDelegate();
2258     syncSyncView();
2259 
2260     syncRebuildOptions();
2261 }
2262 
syncRebuildOptions()2263 void QQuickTableViewPrivate::syncRebuildOptions()
2264 {
2265     if (!scheduledRebuildOptions)
2266         return;
2267 
2268     rebuildState = RebuildState::Begin;
2269     rebuildOptions = scheduledRebuildOptions;
2270     scheduledRebuildOptions = RebuildOption::None;
2271 
2272     if (loadedItems.isEmpty())
2273         rebuildOptions.setFlag(RebuildOption::All);
2274 
2275     // Some options are exclusive:
2276     if (rebuildOptions.testFlag(RebuildOption::All)) {
2277         rebuildOptions.setFlag(RebuildOption::ViewportOnly, false);
2278         rebuildOptions.setFlag(RebuildOption::LayoutOnly, false);
2279         rebuildOptions.setFlag(RebuildOption::CalculateNewContentWidth);
2280         rebuildOptions.setFlag(RebuildOption::CalculateNewContentHeight);
2281     } else if (rebuildOptions.testFlag(RebuildOption::ViewportOnly)) {
2282         rebuildOptions.setFlag(RebuildOption::LayoutOnly, false);
2283     }
2284 }
2285 
syncDelegate()2286 void QQuickTableViewPrivate::syncDelegate()
2287 {
2288     if (!tableModel) {
2289         // Only the tableModel uses the delegate assigned to a
2290         // TableView. DelegateModel has it's own delegate, and
2291         // ObjectModel etc. doesn't use one.
2292         return;
2293     }
2294 
2295     if (assignedDelegate != tableModel->delegate())
2296         tableModel->setDelegate(assignedDelegate);
2297 }
2298 
modelImpl() const2299 QVariant QQuickTableViewPrivate::modelImpl() const
2300 {
2301     return assignedModel;
2302 }
2303 
setModelImpl(const QVariant & newModel)2304 void QQuickTableViewPrivate::setModelImpl(const QVariant &newModel)
2305 {
2306     if (newModel == assignedModel)
2307         return;
2308 
2309     assignedModel = newModel;
2310     scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::All);
2311     emit q_func()->modelChanged();
2312 }
2313 
syncModel()2314 void QQuickTableViewPrivate::syncModel()
2315 {
2316     if (modelVariant == assignedModel)
2317         return;
2318 
2319     if (model) {
2320         disconnectFromModel();
2321         releaseLoadedItems(QQmlTableInstanceModel::NotReusable);
2322     }
2323 
2324     modelVariant = assignedModel;
2325     QVariant effectiveModelVariant = modelVariant;
2326     if (effectiveModelVariant.userType() == qMetaTypeId<QJSValue>())
2327         effectiveModelVariant = effectiveModelVariant.value<QJSValue>().toVariant();
2328 
2329     const auto instanceModel = qobject_cast<QQmlInstanceModel *>(qvariant_cast<QObject*>(effectiveModelVariant));
2330 
2331     if (instanceModel) {
2332         if (tableModel) {
2333             delete tableModel;
2334             tableModel = nullptr;
2335         }
2336         model = instanceModel;
2337     } else {
2338         if (!tableModel)
2339             createWrapperModel();
2340         tableModel->setModel(effectiveModelVariant);
2341     }
2342 
2343     connectToModel();
2344 }
2345 
syncSyncView()2346 void QQuickTableViewPrivate::syncSyncView()
2347 {
2348     Q_Q(QQuickTableView);
2349 
2350     if (assignedSyncView != syncView) {
2351         if (syncView)
2352             syncView->d_func()->syncChildren.removeOne(q);
2353 
2354         if (assignedSyncView) {
2355             QQuickTableView *view = assignedSyncView;
2356 
2357             while (view) {
2358                 if (view == q) {
2359                     if (!layoutWarningIssued) {
2360                         layoutWarningIssued = true;
2361                         qmlWarning(q) << "TableView: recursive syncView connection detected!";
2362                     }
2363                     syncView = nullptr;
2364                     return;
2365                 }
2366                 view = view->d_func()->syncView;
2367             }
2368 
2369             assignedSyncView->d_func()->syncChildren.append(q);
2370             scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2371         }
2372 
2373         syncView = assignedSyncView;
2374     }
2375 
2376     syncHorizontally = syncView && assignedSyncDirection & Qt::Horizontal;
2377     syncVertically = syncView && assignedSyncDirection & Qt::Vertical;
2378 
2379     if (syncHorizontally) {
2380         q->setColumnSpacing(syncView->columnSpacing());
2381         updateContentWidth();
2382     }
2383 
2384     if (syncVertically) {
2385         q->setRowSpacing(syncView->rowSpacing());
2386         updateContentHeight();
2387     }
2388 
2389     if (syncView && loadedItems.isEmpty() && !tableSize.isEmpty()) {
2390         // When we have a syncView, we can sometimes temporarily end up with no loaded items.
2391         // This can happen if the syncView has a model with more rows or columns than us, in
2392         // which case the viewport can end up in a place where we have no rows or columns to
2393         // show. In that case, check now if the viewport has been flicked back again, and
2394         // that we can rebuild the table with a visible top-left cell.
2395         const auto syncView_d = syncView->d_func();
2396         if (!syncView_d->loadedItems.isEmpty()) {
2397             if (syncHorizontally && syncView_d->leftColumn() <= tableSize.width() - 1)
2398                 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::ViewportOnly;
2399             else if (syncVertically && syncView_d->topRow() <= tableSize.height() - 1)
2400                 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::ViewportOnly;
2401         }
2402     }
2403 }
2404 
connectToModel()2405 void QQuickTableViewPrivate::connectToModel()
2406 {
2407     Q_Q(QQuickTableView);
2408     Q_TABLEVIEW_ASSERT(model, "");
2409 
2410     QObjectPrivate::connect(model, &QQmlInstanceModel::createdItem, this, &QQuickTableViewPrivate::itemCreatedCallback);
2411     QObjectPrivate::connect(model, &QQmlInstanceModel::initItem, this, &QQuickTableViewPrivate::initItemCallback);
2412     QObjectPrivate::connect(model, &QQmlTableInstanceModel::itemPooled, this, &QQuickTableViewPrivate::itemPooledCallback);
2413     QObjectPrivate::connect(model, &QQmlTableInstanceModel::itemReused, this, &QQuickTableViewPrivate::itemReusedCallback);
2414 
2415     // Connect atYEndChanged to a function that fetches data if more is available
2416     QObjectPrivate::connect(q, &QQuickTableView::atYEndChanged, this, &QQuickTableViewPrivate::fetchMoreData);
2417 
2418     if (auto const aim = model->abstractItemModel()) {
2419         // When the model exposes a QAIM, we connect to it directly. This means that if the current model is
2420         // a QQmlDelegateModel, we just ignore all the change sets it emits. In most cases, the model will instead
2421         // be our own QQmlTableInstanceModel, which doesn't bother creating change sets at all. For models that are
2422         // not based on QAIM (like QQmlObjectModel, QQmlListModel, javascript arrays etc), there is currently no way
2423         // to modify the model at runtime without also re-setting the model on the view.
2424         connect(aim, &QAbstractItemModel::rowsMoved, this, &QQuickTableViewPrivate::rowsMovedCallback);
2425         connect(aim, &QAbstractItemModel::columnsMoved, this, &QQuickTableViewPrivate::columnsMovedCallback);
2426         connect(aim, &QAbstractItemModel::rowsInserted, this, &QQuickTableViewPrivate::rowsInsertedCallback);
2427         connect(aim, &QAbstractItemModel::rowsRemoved, this, &QQuickTableViewPrivate::rowsRemovedCallback);
2428         connect(aim, &QAbstractItemModel::columnsInserted, this, &QQuickTableViewPrivate::columnsInsertedCallback);
2429         connect(aim, &QAbstractItemModel::columnsRemoved, this, &QQuickTableViewPrivate::columnsRemovedCallback);
2430         connect(aim, &QAbstractItemModel::modelReset, this, &QQuickTableViewPrivate::modelResetCallback);
2431         connect(aim, &QAbstractItemModel::layoutChanged, this, &QQuickTableViewPrivate::layoutChangedCallback);
2432     } else {
2433         QObjectPrivate::connect(model, &QQmlInstanceModel::modelUpdated, this, &QQuickTableViewPrivate::modelUpdated);
2434     }
2435 }
2436 
disconnectFromModel()2437 void QQuickTableViewPrivate::disconnectFromModel()
2438 {
2439     Q_Q(QQuickTableView);
2440     Q_TABLEVIEW_ASSERT(model, "");
2441 
2442     QObjectPrivate::disconnect(model, &QQmlInstanceModel::createdItem, this, &QQuickTableViewPrivate::itemCreatedCallback);
2443     QObjectPrivate::disconnect(model, &QQmlInstanceModel::initItem, this, &QQuickTableViewPrivate::initItemCallback);
2444     QObjectPrivate::disconnect(model, &QQmlTableInstanceModel::itemPooled, this, &QQuickTableViewPrivate::itemPooledCallback);
2445     QObjectPrivate::disconnect(model, &QQmlTableInstanceModel::itemReused, this, &QQuickTableViewPrivate::itemReusedCallback);
2446 
2447     QObjectPrivate::disconnect(q, &QQuickTableView::atYEndChanged, this, &QQuickTableViewPrivate::fetchMoreData);
2448 
2449     if (auto const aim = model->abstractItemModel()) {
2450         disconnect(aim, &QAbstractItemModel::rowsMoved, this, &QQuickTableViewPrivate::rowsMovedCallback);
2451         disconnect(aim, &QAbstractItemModel::columnsMoved, this, &QQuickTableViewPrivate::columnsMovedCallback);
2452         disconnect(aim, &QAbstractItemModel::rowsInserted, this, &QQuickTableViewPrivate::rowsInsertedCallback);
2453         disconnect(aim, &QAbstractItemModel::rowsRemoved, this, &QQuickTableViewPrivate::rowsRemovedCallback);
2454         disconnect(aim, &QAbstractItemModel::columnsInserted, this, &QQuickTableViewPrivate::columnsInsertedCallback);
2455         disconnect(aim, &QAbstractItemModel::columnsRemoved, this, &QQuickTableViewPrivate::columnsRemovedCallback);
2456         disconnect(aim, &QAbstractItemModel::modelReset, this, &QQuickTableViewPrivate::modelResetCallback);
2457         disconnect(aim, &QAbstractItemModel::layoutChanged, this, &QQuickTableViewPrivate::layoutChangedCallback);
2458     } else {
2459         QObjectPrivate::disconnect(model, &QQmlInstanceModel::modelUpdated, this, &QQuickTableViewPrivate::modelUpdated);
2460     }
2461 }
2462 
modelUpdated(const QQmlChangeSet & changeSet,bool reset)2463 void QQuickTableViewPrivate::modelUpdated(const QQmlChangeSet &changeSet, bool reset)
2464 {
2465     Q_UNUSED(changeSet);
2466     Q_UNUSED(reset);
2467 
2468     Q_TABLEVIEW_ASSERT(!model->abstractItemModel(), "");
2469     scheduleRebuildTable(RebuildOption::ViewportOnly
2470                          | RebuildOption::CalculateNewContentWidth
2471                          | RebuildOption::CalculateNewContentHeight);
2472 }
2473 
rowsMovedCallback(const QModelIndex & parent,int,int,const QModelIndex &,int)2474 void QQuickTableViewPrivate::rowsMovedCallback(const QModelIndex &parent, int, int, const QModelIndex &, int )
2475 {
2476     if (parent != QModelIndex())
2477         return;
2478 
2479     scheduleRebuildTable(RebuildOption::ViewportOnly);
2480 }
2481 
columnsMovedCallback(const QModelIndex & parent,int,int,const QModelIndex &,int)2482 void QQuickTableViewPrivate::columnsMovedCallback(const QModelIndex &parent, int, int, const QModelIndex &, int)
2483 {
2484     if (parent != QModelIndex())
2485         return;
2486 
2487     scheduleRebuildTable(RebuildOption::ViewportOnly);
2488 }
2489 
rowsInsertedCallback(const QModelIndex & parent,int,int)2490 void QQuickTableViewPrivate::rowsInsertedCallback(const QModelIndex &parent, int, int)
2491 {
2492     if (parent != QModelIndex())
2493         return;
2494 
2495     scheduleRebuildTable(RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
2496 }
2497 
rowsRemovedCallback(const QModelIndex & parent,int,int)2498 void QQuickTableViewPrivate::rowsRemovedCallback(const QModelIndex &parent, int, int)
2499 {
2500     if (parent != QModelIndex())
2501         return;
2502 
2503     scheduleRebuildTable(RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
2504 }
2505 
columnsInsertedCallback(const QModelIndex & parent,int,int)2506 void QQuickTableViewPrivate::columnsInsertedCallback(const QModelIndex &parent, int, int)
2507 {
2508     if (parent != QModelIndex())
2509         return;
2510 
2511     // Adding a column (or row) can result in the table going from being
2512     // e.g completely inside the viewport to go outside. And in the latter
2513     // case, the user needs to be able to scroll the viewport, also if
2514     // flags such as Flickable.StopAtBounds is in use. So we need to
2515     // update contentWidth to support that case.
2516     scheduleRebuildTable(RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
2517 }
2518 
columnsRemovedCallback(const QModelIndex & parent,int,int)2519 void QQuickTableViewPrivate::columnsRemovedCallback(const QModelIndex &parent, int, int)
2520 {
2521     if (parent != QModelIndex())
2522         return;
2523 
2524     scheduleRebuildTable(RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
2525 }
2526 
layoutChangedCallback(const QList<QPersistentModelIndex> & parents,QAbstractItemModel::LayoutChangeHint hint)2527 void QQuickTableViewPrivate::layoutChangedCallback(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint)
2528 {
2529     Q_UNUSED(parents);
2530     Q_UNUSED(hint);
2531 
2532     scheduleRebuildTable(RebuildOption::ViewportOnly);
2533 }
2534 
fetchMoreData()2535 void QQuickTableViewPrivate::fetchMoreData()
2536 {
2537     if (tableModel && tableModel->canFetchMore()) {
2538         tableModel->fetchMore();
2539         scheduleRebuildTable(RebuildOption::ViewportOnly);
2540     }
2541 }
2542 
modelResetCallback()2543 void QQuickTableViewPrivate::modelResetCallback()
2544 {
2545     scheduleRebuildTable(RebuildOption::All);
2546 }
2547 
scheduleRebuildIfFastFlick()2548 void QQuickTableViewPrivate::scheduleRebuildIfFastFlick()
2549 {
2550     Q_Q(QQuickTableView);
2551     // If the viewport has moved more than one page vertically or horizontally, we switch
2552     // strategy from refilling edges around the current table to instead rebuild the table
2553     // from scratch inside the new viewport. This will greatly improve performance when flicking
2554     // a long distance in one go, which can easily happen when dragging on scrollbars.
2555     // Note that we don't want to update the content size in this case, since first of all, the
2556     // content size should logically not change as a result of flicking. But more importantly, updating
2557     // the content size in combination with fast-flicking has a tendency to cause flicker in the viewport.
2558 
2559     // Check the viewport moved more than one page vertically
2560     if (!viewportRect.intersects(QRectF(viewportRect.x(), q->contentY(), 1, q->height()))) {
2561         scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow;
2562         scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2563     }
2564 
2565     // Check the viewport moved more than one page horizontally
2566     if (!viewportRect.intersects(QRectF(q->contentX(), viewportRect.y(), q->width(), 1))) {
2567         scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn;
2568         scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2569     }
2570 }
2571 
setLocalViewportX(qreal contentX)2572 void QQuickTableViewPrivate::setLocalViewportX(qreal contentX)
2573 {
2574     // Set the new viewport position if changed, but don't trigger any
2575     // rebuilds or updates. We use this function internally to distinguish
2576     // external flicking from internal sync-ing of the content view.
2577     Q_Q(QQuickTableView);
2578     QBoolBlocker blocker(inSetLocalViewportPos, true);
2579 
2580     if (qFuzzyCompare(contentX, q->contentX()))
2581         return;
2582 
2583     q->setContentX(contentX);
2584 }
2585 
setLocalViewportY(qreal contentY)2586 void QQuickTableViewPrivate::setLocalViewportY(qreal contentY)
2587 {
2588     // Set the new viewport position if changed, but don't trigger any
2589     // rebuilds or updates. We use this function internally to distinguish
2590     // external flicking from internal sync-ing of the content view.
2591     Q_Q(QQuickTableView);
2592     QBoolBlocker blocker(inSetLocalViewportPos, true);
2593 
2594     if (qFuzzyCompare(contentY, q->contentY()))
2595         return;
2596 
2597     q->setContentY(contentY);
2598 }
2599 
syncViewportRect()2600 void QQuickTableViewPrivate::syncViewportRect()
2601 {
2602     // Sync viewportRect so that it contains the actual geometry of the viewport
2603     Q_Q(QQuickTableView);
2604     viewportRect = QRectF(q->contentX(), q->contentY(), q->width(), q->height());
2605     qCDebug(lcTableViewDelegateLifecycle) << viewportRect;
2606 }
2607 
syncViewportPosRecursive()2608 void QQuickTableViewPrivate::syncViewportPosRecursive()
2609 {
2610     Q_Q(QQuickTableView);
2611     QBoolBlocker recursionGuard(inSyncViewportPosRecursive, true);
2612 
2613     if (syncView) {
2614         auto syncView_d = syncView->d_func();
2615         if (!syncView_d->inSyncViewportPosRecursive) {
2616             if (syncHorizontally)
2617                 syncView_d->setLocalViewportX(q->contentX());
2618             if (syncVertically)
2619                 syncView_d->setLocalViewportY(q->contentY());
2620             syncView_d->syncViewportPosRecursive();
2621         }
2622     }
2623 
2624     for (auto syncChild : qAsConst(syncChildren)) {
2625         auto syncChild_d = syncChild->d_func();
2626         if (!syncChild_d->inSyncViewportPosRecursive) {
2627             if (syncChild_d->syncHorizontally)
2628                 syncChild_d->setLocalViewportX(q->contentX());
2629             if (syncChild_d->syncVertically)
2630                 syncChild_d->setLocalViewportY(q->contentY());
2631             syncChild_d->syncViewportPosRecursive();
2632         }
2633     }
2634 }
2635 
QQuickTableView(QQuickItem * parent)2636 QQuickTableView::QQuickTableView(QQuickItem *parent)
2637     : QQuickFlickable(*(new QQuickTableViewPrivate), parent)
2638 {
2639     setFlag(QQuickItem::ItemIsFocusScope);
2640 }
2641 
~QQuickTableView()2642 QQuickTableView::~QQuickTableView()
2643 {
2644 }
2645 
QQuickTableView(QQuickTableViewPrivate & dd,QQuickItem * parent)2646 QQuickTableView::QQuickTableView(QQuickTableViewPrivate &dd, QQuickItem *parent)
2647     : QQuickFlickable(dd, parent)
2648 {
2649     setFlag(QQuickItem::ItemIsFocusScope);
2650 }
2651 
minXExtent() const2652 qreal QQuickTableView::minXExtent() const
2653 {
2654     return QQuickFlickable::minXExtent() - d_func()->origin.x();
2655 }
2656 
maxXExtent() const2657 qreal QQuickTableView::maxXExtent() const
2658 {
2659     return QQuickFlickable::maxXExtent() - d_func()->endExtent.width();
2660 }
2661 
minYExtent() const2662 qreal QQuickTableView::minYExtent() const
2663 {
2664     return QQuickFlickable::minYExtent() - d_func()->origin.y();
2665 }
2666 
maxYExtent() const2667 qreal QQuickTableView::maxYExtent() const
2668 {
2669     return QQuickFlickable::maxYExtent() - d_func()->endExtent.height();
2670 }
2671 
rows() const2672 int QQuickTableView::rows() const
2673 {
2674     return d_func()->tableSize.height();
2675 }
2676 
columns() const2677 int QQuickTableView::columns() const
2678 {
2679     return d_func()->tableSize.width();
2680 }
2681 
rowSpacing() const2682 qreal QQuickTableView::rowSpacing() const
2683 {
2684     return d_func()->cellSpacing.height();
2685 }
2686 
setRowSpacing(qreal spacing)2687 void QQuickTableView::setRowSpacing(qreal spacing)
2688 {
2689     Q_D(QQuickTableView);
2690     if (qt_is_nan(spacing) || !qt_is_finite(spacing))
2691         return;
2692     if (qFuzzyCompare(d->cellSpacing.height(), spacing))
2693         return;
2694 
2695     d->cellSpacing.setHeight(spacing);
2696     d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::LayoutOnly
2697                             | QQuickTableViewPrivate::RebuildOption::CalculateNewContentHeight);
2698     emit rowSpacingChanged();
2699 }
2700 
columnSpacing() const2701 qreal QQuickTableView::columnSpacing() const
2702 {
2703     return d_func()->cellSpacing.width();
2704 }
2705 
setColumnSpacing(qreal spacing)2706 void QQuickTableView::setColumnSpacing(qreal spacing)
2707 {
2708     Q_D(QQuickTableView);
2709     if (qt_is_nan(spacing) || !qt_is_finite(spacing))
2710         return;
2711     if (qFuzzyCompare(d->cellSpacing.width(), spacing))
2712         return;
2713 
2714     d->cellSpacing.setWidth(spacing);
2715     d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::LayoutOnly
2716                             | QQuickTableViewPrivate::RebuildOption::CalculateNewContentWidth);
2717     emit columnSpacingChanged();
2718 }
2719 
rowHeightProvider() const2720 QJSValue QQuickTableView::rowHeightProvider() const
2721 {
2722     return d_func()->rowHeightProvider;
2723 }
2724 
setRowHeightProvider(const QJSValue & provider)2725 void QQuickTableView::setRowHeightProvider(const QJSValue &provider)
2726 {
2727     Q_D(QQuickTableView);
2728     if (provider.strictlyEquals(d->rowHeightProvider))
2729         return;
2730 
2731     d->rowHeightProvider = provider;
2732     d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::ViewportOnly
2733                             | QQuickTableViewPrivate::RebuildOption::CalculateNewContentHeight);
2734     emit rowHeightProviderChanged();
2735 }
2736 
columnWidthProvider() const2737 QJSValue QQuickTableView::columnWidthProvider() const
2738 {
2739     return d_func()->columnWidthProvider;
2740 }
2741 
setColumnWidthProvider(const QJSValue & provider)2742 void QQuickTableView::setColumnWidthProvider(const QJSValue &provider)
2743 {
2744     Q_D(QQuickTableView);
2745     if (provider.strictlyEquals(d->columnWidthProvider))
2746         return;
2747 
2748     d->columnWidthProvider = provider;
2749     d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::ViewportOnly
2750                             | QQuickTableViewPrivate::RebuildOption::CalculateNewContentWidth);
2751     emit columnWidthProviderChanged();
2752 }
2753 
model() const2754 QVariant QQuickTableView::model() const
2755 {
2756     return d_func()->modelImpl();
2757 }
2758 
setModel(const QVariant & newModel)2759 void QQuickTableView::setModel(const QVariant &newModel)
2760 {
2761     return d_func()->setModelImpl(newModel);
2762 }
2763 
delegate() const2764 QQmlComponent *QQuickTableView::delegate() const
2765 {
2766     return d_func()->assignedDelegate;
2767 }
2768 
setDelegate(QQmlComponent * newDelegate)2769 void QQuickTableView::setDelegate(QQmlComponent *newDelegate)
2770 {
2771     Q_D(QQuickTableView);
2772     if (newDelegate == d->assignedDelegate)
2773         return;
2774 
2775     d->assignedDelegate = newDelegate;
2776     d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::All);
2777 
2778     emit delegateChanged();
2779 }
2780 
reuseItems() const2781 bool QQuickTableView::reuseItems() const
2782 {
2783     return bool(d_func()->reusableFlag == QQmlTableInstanceModel::Reusable);
2784 }
2785 
setReuseItems(bool reuse)2786 void QQuickTableView::setReuseItems(bool reuse)
2787 {
2788     Q_D(QQuickTableView);
2789     if (reuseItems() == reuse)
2790         return;
2791 
2792     d->reusableFlag = reuse ? QQmlTableInstanceModel::Reusable : QQmlTableInstanceModel::NotReusable;
2793 
2794     if (!reuse && d->tableModel) {
2795         // When we're told to not reuse items, we
2796         // immediately, as documented, drain the pool.
2797         d->tableModel->drainReusableItemsPool(0);
2798     }
2799 
2800     emit reuseItemsChanged();
2801 }
2802 
setContentWidth(qreal width)2803 void QQuickTableView::setContentWidth(qreal width)
2804 {
2805     Q_D(QQuickTableView);
2806     d->explicitContentWidth = width;
2807     QQuickFlickable::setContentWidth(width);
2808 }
2809 
setContentHeight(qreal height)2810 void QQuickTableView::setContentHeight(qreal height)
2811 {
2812     Q_D(QQuickTableView);
2813     d->explicitContentHeight = height;
2814     QQuickFlickable::setContentHeight(height);
2815 }
2816 
2817 /*!
2818     \qmlproperty TableView QtQuick::TableView::syncView
2819 
2820     If this property of a TableView is set to another TableView, both the
2821     tables will synchronize with regard to flicking, column widths/row heights,
2822     and spacing according to \l syncDirection.
2823 
2824     If \l syncDirection contains \l Qt.Horizontal, current tableView's column
2825     widths, column spacing, and horizontal flicking movement synchronizes with
2826     syncView's.
2827 
2828     If \l syncDirection contains \l Qt.Vertical, current tableView's row
2829     heights, row spacing, and vertical flicking movement synchronizes with
2830     syncView's.
2831 
2832     \sa syncDirection
2833 */
syncView() const2834 QQuickTableView *QQuickTableView::syncView() const
2835 {
2836    return d_func()->assignedSyncView;
2837 }
2838 
setSyncView(QQuickTableView * view)2839 void QQuickTableView::setSyncView(QQuickTableView *view)
2840 {
2841     Q_D(QQuickTableView);
2842     if (d->assignedSyncView == view)
2843         return;
2844 
2845     d->assignedSyncView = view;
2846     d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::ViewportOnly);
2847 
2848     emit syncViewChanged();
2849 }
2850 
2851 /*!
2852     \qmlproperty Qt::Orientations QtQuick::TableView::syncDirection
2853 
2854     If the \l syncView is set on a TableView, this property controls
2855     synchronization of flicking direction(s) for both tables. The default is \c
2856     {Qt.Horizontal | Qt.Vertical}, which means that if you flick either table
2857     in either direction, the other table is flicked the same amount in the
2858     same direction.
2859 
2860     This property and \l syncView can be used to make two tableViews
2861     synchronize with each other smoothly in flicking regardless of the different
2862     overshoot/undershoot, velocity, acceleration/deceleration or rebound
2863     animation, and so on.
2864 
2865     A typical use case is to make several headers flick along with the table.
2866 
2867     \sa syncView, headerView
2868 */
syncDirection() const2869 Qt::Orientations QQuickTableView::syncDirection() const
2870 {
2871    return d_func()->assignedSyncDirection;
2872 }
2873 
setSyncDirection(Qt::Orientations direction)2874 void QQuickTableView::setSyncDirection(Qt::Orientations direction)
2875 {
2876     Q_D(QQuickTableView);
2877     if (d->assignedSyncDirection == direction)
2878         return;
2879 
2880     d->assignedSyncDirection = direction;
2881     if (d->assignedSyncView)
2882         d->scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::ViewportOnly);
2883 
2884     emit syncDirectionChanged();
2885 }
2886 
forceLayout()2887 void QQuickTableView::forceLayout()
2888 {
2889     d_func()->forceLayout();
2890 }
2891 
qmlAttachedProperties(QObject * obj)2892 QQuickTableViewAttached *QQuickTableView::qmlAttachedProperties(QObject *obj)
2893 {
2894     return new QQuickTableViewAttached(obj);
2895 }
2896 
geometryChanged(const QRectF & newGeometry,const QRectF & oldGeometry)2897 void QQuickTableView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
2898 {
2899     Q_D(QQuickTableView);
2900     QQuickFlickable::geometryChanged(newGeometry, oldGeometry);
2901 
2902     if (d->tableModel) {
2903         // When the view changes size, we force the pool to
2904         // shrink by releasing all pooled items.
2905         d->tableModel->drainReusableItemsPool(0);
2906     }
2907 
2908     polish();
2909 }
2910 
viewportMoved(Qt::Orientations orientation)2911 void QQuickTableView::viewportMoved(Qt::Orientations orientation)
2912 {
2913     Q_D(QQuickTableView);
2914 
2915     // If the new viewport position was set from the setLocalViewportXY()
2916     // functions, we just update the position silently and return. Otherwise, if
2917     // the viewport was flicked by the user, or some other control, we
2918     // recursively sync all the views in the hierarchy to the same position.
2919     QQuickFlickable::viewportMoved(orientation);
2920     if (d->inSetLocalViewportPos)
2921         return;
2922 
2923     // Move all views in the syncView hierarchy to the same contentX/Y.
2924     // We need to start from this view (and not the root syncView) to
2925     // ensure that we respect all the individual syncDirection flags
2926     // between the individual views in the hierarchy.
2927     d->syncViewportPosRecursive();
2928 
2929     auto rootView = d->rootSyncView();
2930     auto rootView_d = rootView->d_func();
2931 
2932     rootView_d->scheduleRebuildIfFastFlick();
2933 
2934     if (!rootView_d->polishScheduled) {
2935         if (rootView_d->scheduledRebuildOptions) {
2936             // When we need to rebuild, collecting several viewport
2937             // moves and do a single polish gives a quicker UI.
2938             rootView->polish();
2939         } else {
2940             // Updating the table right away when flicking
2941             // slowly gives a smoother experience.
2942             const bool updated = rootView->d_func()->updateTableRecursive();
2943             if (!updated) {
2944                 // One, or more, of the views are already in an
2945                 // update, so we need to wait a cycle.
2946                 rootView->polish();
2947             }
2948         }
2949     }
2950 }
2951 
_q_componentFinalized()2952 void QQuickTableViewPrivate::_q_componentFinalized()
2953 {
2954     // Now that all bindings are evaluated, and we know
2955     // our final geometery, we can build the table.
2956     qCDebug(lcTableViewDelegateLifecycle);
2957     updatePolish();
2958 }
2959 
registerCallbackWhenBindingsAreEvaluated()2960 void QQuickTableViewPrivate::registerCallbackWhenBindingsAreEvaluated()
2961 {
2962     // componentComplete() is called on us after all static values have been assigned, but
2963     // before bindings to any anchestors has been evaluated. Especially this means that
2964     // if our size is bound to the parents size, it will still be empty at that point.
2965     // And we cannot build the table without knowing our own size. We could wait until we
2966     // got the first updatePolish() callback, but at that time, any asynchronous loaders that we
2967     // might be inside have already finished loading, which means that we would load all
2968     // the delegate items synchronously instead of asynchronously. We therefore add a componentFinalized
2969     // function that gets called after all the bindings we rely on has been evaluated.
2970     // When receiving this call, we load the delegate items (and build the table).
2971     Q_Q(QQuickTableView);
2972     QQmlEnginePrivate *engPriv = QQmlEnginePrivate::get(qmlEngine(q));
2973     static int finalizedIdx = -1;
2974     if (finalizedIdx < 0)
2975         finalizedIdx = q->metaObject()->indexOfSlot("_q_componentFinalized()");
2976     engPriv->registerFinalizeCallback(q, finalizedIdx);
2977 }
2978 
componentComplete()2979 void QQuickTableView::componentComplete()
2980 {
2981     QQuickFlickable::componentComplete();
2982     d_func()->registerCallbackWhenBindingsAreEvaluated();
2983 }
2984 
2985 class QObjectPrivate;
2986 class QQuickTableSectionSizeProviderPrivate : public QObjectPrivate {
2987 public:
2988     QQuickTableSectionSizeProviderPrivate();
2989     ~QQuickTableSectionSizeProviderPrivate();
2990     QHash<int, qreal> hash;
2991 };
2992 
QQuickTableSectionSizeProvider(QObject * parent)2993 QQuickTableSectionSizeProvider::QQuickTableSectionSizeProvider(QObject *parent)
2994     : QObject (*(new QQuickTableSectionSizeProviderPrivate), parent)
2995 {
2996 }
2997 
setSize(int section,qreal size)2998 void QQuickTableSectionSizeProvider::setSize(int section, qreal size)
2999 {
3000     Q_D(QQuickTableSectionSizeProvider);
3001     if (section < 0 || size < 0) {
3002         qmlWarning(this) << "setSize: section or size less than zero";
3003         return;
3004     }
3005     if (qFuzzyCompare(QQuickTableSectionSizeProvider::size(section), size))
3006         return;
3007     d->hash.insert(section, size);
3008     emit sizeChanged();
3009 }
3010 
3011 // return -1.0 if no valid explicit size retrieved
size(int section)3012 qreal QQuickTableSectionSizeProvider::size(int section)
3013 {
3014     Q_D(QQuickTableSectionSizeProvider);
3015     auto it = d->hash.find(section);
3016     if (it != d->hash.end())
3017         return *it;
3018     return -1.0;
3019 }
3020 
3021 // return true if section is valid
resetSize(int section)3022 bool QQuickTableSectionSizeProvider::resetSize(int section)
3023 {
3024     Q_D(QQuickTableSectionSizeProvider);
3025     if (d->hash.empty())
3026         return false;
3027 
3028     auto ret = d->hash.remove(section);
3029     if (ret)
3030         emit sizeChanged();
3031     return ret;
3032 }
3033 
resetAll()3034 void QQuickTableSectionSizeProvider::resetAll()
3035 {
3036     Q_D(QQuickTableSectionSizeProvider);
3037     d->hash.clear();
3038     emit sizeChanged();
3039 }
3040 
QQuickTableSectionSizeProviderPrivate()3041 QQuickTableSectionSizeProviderPrivate::QQuickTableSectionSizeProviderPrivate()
3042     : QObjectPrivate()
3043 {
3044 }
3045 
~QQuickTableSectionSizeProviderPrivate()3046 QQuickTableSectionSizeProviderPrivate::~QQuickTableSectionSizeProviderPrivate()
3047 {
3048 
3049 }
3050 #include "moc_qquicktableview_p.cpp"
3051 
3052 QT_END_NAMESPACE
3053