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
395QT_BEGIN_NAMESPACE
396
397Q_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
402static const Qt::Edge allTableEdges[] = { Qt::LeftEdge, Qt::RightEdge, Qt::TopEdge, Qt::BottomEdge };
403static const int kEdgeIndexNotSet = -2;
404static const int kEdgeIndexAtEnd = -3;
405
406const QPoint QQuickTableViewPrivate::kLeft = QPoint(-1, 0);
407const QPoint QQuickTableViewPrivate::kRight = QPoint(1, 0);
408const QPoint QQuickTableViewPrivate::kUp = QPoint(0, -1);
409const QPoint QQuickTableViewPrivate::kDown = QPoint(0, 1);
410
411QQuickTableViewPrivate::EdgeRange::EdgeRange()
412 : startIndex(kEdgeIndexNotSet)
413 , endIndex(kEdgeIndexNotSet)
414 , size(0)
415{}
416
417bool 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(a: startIndex, b: endIndex);
434 const int e = std::max(a: startIndex, b: endIndex);
435 return index >= s && index <= e;
436}
437
438QQuickTableViewPrivate::QQuickTableViewPrivate()
439 : QQuickFlickablePrivate()
440{
441 QObject::connect(sender: &columnWidths, signal: &QQuickTableSectionSizeProvider::sizeChanged,
442 slot: [this] { this->forceLayout();});
443 QObject::connect(sender: &rowHeights, signal: &QQuickTableSectionSizeProvider::sizeChanged,
444 slot: [this] { this->forceLayout();});
445}
446
447QQuickTableViewPrivate::~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(object: item);
455 }
456 delete fxTableItem;
457 }
458
459 if (tableModel)
460 delete tableModel;
461}
462
463QString 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(a: leftColumn()).arg(a: topRow())
469 .arg(a: rightColumn()).arg(a: bottomRow())
470 .arg(a: loadedItems.count())
471 .arg(a: loadedTableOuterRect.x())
472 .arg(a: loadedTableOuterRect.y())
473 .arg(a: loadedTableOuterRect.width())
474 .arg(a: loadedTableOuterRect.height());
475}
476
477void QQuickTableViewPrivate::dumpTable() const
478{
479 auto listCopy = loadedItems.values();
480 std::stable_sort(first: listCopy.begin(), last: listCopy.end(),
481 comp: [](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: filename);
491 if (q_func()->window() && q_func()->window()->grabWindow().save(fileName: path))
492 qWarning() << "Window capture saved to:" << path;
493}
494
495QQuickTableViewAttached *QQuickTableViewPrivate::getAttachedObject(const QObject *object) const
496{
497 QObject *attachedObject = qmlAttachedPropertiesObject<QQuickTableView>(obj: object);
498 return static_cast<QQuickTableViewAttached *>(attachedObject);
499}
500
501int 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
515QPoint 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
533int QQuickTableViewPrivate::edgeToArrayIndex(Qt::Edge edge)
534{
535 return int(log2(x: float(edge)));
536}
537
538void 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
547int 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
562int 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, index: 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(column: 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(column: 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(row: 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(row: 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
645bool QQuickTableViewPrivate::allColumnsLoaded()
646{
647 // Returns true if all the columns in the model (that are not
648 // hidden by the columnWidthProvider) are currently loaded and visible.
649 const bool firstColumnLoaded = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::LeftEdge) == kEdgeIndexAtEnd;
650 if (!firstColumnLoaded)
651 return false;
652 bool lastColumnLoaded = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::RightEdge) == kEdgeIndexAtEnd;
653 return lastColumnLoaded;
654}
655
656bool QQuickTableViewPrivate::allRowsLoaded()
657{
658 // Returns true if all the rows in the model (that are not hidden
659 // by the columnWidthProvider) are currently loaded and visible.
660 const bool firstColumnLoaded = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::TopEdge) == kEdgeIndexAtEnd;
661 if (!firstColumnLoaded)
662 return false;
663 bool lastColumnLoaded = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::BottomEdge) == kEdgeIndexAtEnd;
664 return lastColumnLoaded;
665}
666
667void QQuickTableViewPrivate::updateContentWidth()
668{
669 // Note that we actually never really know what the content size / size of the full table will
670 // be. Even if e.g spacing changes, and we normally would assume that the size of the table
671 // would increase accordingly, the model might also at some point have removed/hidden/resized
672 // rows/columns outside the viewport. This would also affect the size, but since we don't load
673 // rows or columns outside the viewport, this information is ignored. And even if we did, we
674 // might also have been fast-flicked to a new location at some point, and started a new rebuild
675 // there based on a new guesstimated top-left cell. So the calculated content size should always
676 // be understood as a guesstimate, which sometimes can be really off (as a tradeoff for performance).
677 // When this is not acceptable, the user can always set a custom content size explicitly.
678 Q_Q(QQuickTableView);
679
680 if (syncHorizontally) {
681 QBoolBlocker fixupGuard(inUpdateContentSize, true);
682 q->QQuickFlickable::setContentWidth(syncView->contentWidth());
683 return;
684 }
685
686 if (explicitContentWidth.isValid()) {
687 // Don't calculate contentWidth when it
688 // was set explicitly by the application.
689 return;
690 }
691
692 if (loadedItems.isEmpty()) {
693 QBoolBlocker fixupGuard(inUpdateContentSize, true);
694 q->QQuickFlickable::setContentWidth(0);
695 return;
696 }
697
698 const int nextColumn = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::RightEdge);
699 const int columnsRemaining = nextColumn == kEdgeIndexAtEnd ? 0 : tableSize.width() - nextColumn;
700 const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
701 const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
702 const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
703 const qreal estimatedWidth = loadedTableOuterRect.right() + estimatedRemainingWidth;
704
705 QBoolBlocker fixupGuard(inUpdateContentSize, true);
706 q->QQuickFlickable::setContentWidth(estimatedWidth);
707}
708
709void QQuickTableViewPrivate::updateContentHeight()
710{
711 Q_Q(QQuickTableView);
712
713 if (syncVertically) {
714 QBoolBlocker fixupGuard(inUpdateContentSize, true);
715 q->QQuickFlickable::setContentHeight(syncView->contentHeight());
716 return;
717 }
718
719 if (explicitContentHeight.isValid()) {
720 // Don't calculate contentHeight when it
721 // was set explicitly by the application.
722 return;
723 }
724
725 if (loadedItems.isEmpty()) {
726 QBoolBlocker fixupGuard(inUpdateContentSize, true);
727 q->QQuickFlickable::setContentHeight(0);
728 return;
729 }
730
731 const int nextRow = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::BottomEdge);
732 const int rowsRemaining = nextRow == kEdgeIndexAtEnd ? 0 : tableSize.height() - nextRow;
733 const qreal remainingRowHeights = rowsRemaining * averageEdgeSize.height();
734 const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
735 const qreal estimatedRemainingHeight = remainingRowHeights + remainingSpacing;
736 const qreal estimatedHeight = loadedTableOuterRect.bottom() + estimatedRemainingHeight;
737
738 QBoolBlocker fixupGuard(inUpdateContentSize, true);
739 q->QQuickFlickable::setContentHeight(estimatedHeight);
740}
741
742void QQuickTableViewPrivate::updateExtents()
743{
744 // When rows or columns outside the viewport are removed or added, or a rebuild
745 // forces us to guesstimate a new top-left, the edges of the table might end up
746 // out of sync with the edges of the content view. We detect this situation here, and
747 // move the origin to ensure that there will never be gaps at the end of the table.
748 // Normally we detect that the size of the whole table is not going to be equal to the
749 // size of the content view already when we load the last row/column, and especially
750 // before it's flicked completely inside the viewport. For those cases we simply adjust
751 // the origin/endExtent, to give a smooth flicking experience.
752 // But if flicking fast (e.g with a scrollbar), it can happen that the viewport ends up
753 // outside the end of the table in just one viewport update. To avoid a "blink" in the
754 // viewport when that happens, we "move" the loaded table into the viewport to cover it.
755 Q_Q(QQuickTableView);
756
757 bool tableMovedHorizontally = false;
758 bool tableMovedVertically = false;
759
760 const int nextLeftColumn = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::LeftEdge);
761 const int nextRightColumn = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::RightEdge);
762 const int nextTopRow = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::TopEdge);
763 const int nextBottomRow = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::BottomEdge);
764
765 if (syncHorizontally) {
766 const auto syncView_d = syncView->d_func();
767 origin.rx() = syncView_d->origin.x();
768 endExtent.rwidth() = syncView_d->endExtent.width();
769 hData.markExtentsDirty();
770 } else if (nextLeftColumn == kEdgeIndexAtEnd) {
771 // There are no more columns to load on the left side of the table.
772 // In that case, we ensure that the origin match the beginning of the table.
773 if (loadedTableOuterRect.left() > viewportRect.left()) {
774 // We have a blank area at the left end of the viewport. In that case we don't have time to
775 // wait for the viewport to move (after changing origin), since that will take an extra
776 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
777 // us overshooting, we brute force the loaded table inside the already existing viewport.
778 if (loadedTableOuterRect.left() > origin.x()) {
779 const qreal diff = loadedTableOuterRect.left() - origin.x();
780 loadedTableOuterRect.moveLeft(pos: loadedTableOuterRect.left() - diff);
781 loadedTableInnerRect.moveLeft(pos: loadedTableInnerRect.left() - diff);
782 tableMovedHorizontally = true;
783 }
784 }
785 origin.rx() = loadedTableOuterRect.left();
786 hData.markExtentsDirty();
787 } else if (loadedTableOuterRect.left() <= origin.x() + cellSpacing.width()) {
788 // The table rect is at the origin, or outside, but we still have more
789 // visible columns to the left. So we try to guesstimate how much space
790 // the rest of the columns will occupy, and move the origin accordingly.
791 const int columnsRemaining = nextLeftColumn + 1;
792 const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
793 const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
794 const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
795 origin.rx() = loadedTableOuterRect.left() - estimatedRemainingWidth;
796 hData.markExtentsDirty();
797 } else if (nextRightColumn == kEdgeIndexAtEnd) {
798 // There are no more columns to load on the right side of the table.
799 // In that case, we ensure that the end of the content view match the end of the table.
800 if (loadedTableOuterRect.right() < viewportRect.right()) {
801 // We have a blank area at the right end of the viewport. In that case we don't have time to
802 // wait for the viewport to move (after changing endExtent), since that will take an extra
803 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
804 // us overshooting, we brute force the loaded table inside the already existing viewport.
805 const qreal w = qMin(a: viewportRect.right(), b: q->contentWidth() + endExtent.width());
806 if (loadedTableOuterRect.right() < w) {
807 const qreal diff = loadedTableOuterRect.right() - w;
808 loadedTableOuterRect.moveRight(pos: loadedTableOuterRect.right() - diff);
809 loadedTableInnerRect.moveRight(pos: loadedTableInnerRect.right() - diff);
810 tableMovedHorizontally = true;
811 }
812 }
813 endExtent.rwidth() = loadedTableOuterRect.right() - q->contentWidth();
814 hData.markExtentsDirty();
815 } else if (loadedTableOuterRect.right() >= q->contentWidth() + endExtent.width() - cellSpacing.width()) {
816 // The right-most column is outside the end of the content view, and we
817 // still have more visible columns in the model. This can happen if the application
818 // has set a fixed content width.
819 const int columnsRemaining = tableSize.width() - nextRightColumn;
820 const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
821 const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
822 const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
823 const qreal pixelsOutsideContentWidth = loadedTableOuterRect.right() - q->contentWidth();
824 endExtent.rwidth() = pixelsOutsideContentWidth + estimatedRemainingWidth;
825 hData.markExtentsDirty();
826 }
827
828 if (syncVertically) {
829 const auto syncView_d = syncView->d_func();
830 origin.ry() = syncView_d->origin.y();
831 endExtent.rheight() = syncView_d->endExtent.height();
832 vData.markExtentsDirty();
833 } else if (nextTopRow == kEdgeIndexAtEnd) {
834 // There are no more rows to load on the top side of the table.
835 // In that case, we ensure that the origin match the beginning of the table.
836 if (loadedTableOuterRect.top() > viewportRect.top()) {
837 // We have a blank area at the top of the viewport. In that case we don't have time to
838 // wait for the viewport to move (after changing origin), since that will take an extra
839 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
840 // us overshooting, we brute force the loaded table inside the already existing viewport.
841 if (loadedTableOuterRect.top() > origin.y()) {
842 const qreal diff = loadedTableOuterRect.top() - origin.y();
843 loadedTableOuterRect.moveTop(pos: loadedTableOuterRect.top() - diff);
844 loadedTableInnerRect.moveTop(pos: loadedTableInnerRect.top() - diff);
845 tableMovedVertically = true;
846 }
847 }
848 origin.ry() = loadedTableOuterRect.top();
849 vData.markExtentsDirty();
850 } else if (loadedTableOuterRect.top() <= origin.y() + cellSpacing.height()) {
851 // The table rect is at the origin, or outside, but we still have more
852 // visible rows at the top. So we try to guesstimate how much space
853 // the rest of the rows will occupy, and move the origin accordingly.
854 const int rowsRemaining = nextTopRow + 1;
855 const qreal remainingRowHeights = rowsRemaining * averageEdgeSize.height();
856 const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
857 const qreal estimatedRemainingHeight = remainingRowHeights + remainingSpacing;
858 origin.ry() = loadedTableOuterRect.top() - estimatedRemainingHeight;
859 vData.markExtentsDirty();
860 } else if (nextBottomRow == kEdgeIndexAtEnd) {
861 // There are no more rows to load on the bottom side of the table.
862 // In that case, we ensure that the end of the content view match the end of the table.
863 if (loadedTableOuterRect.bottom() < viewportRect.bottom()) {
864 // We have a blank area at the bottom of the viewport. In that case we don't have time to
865 // wait for the viewport to move (after changing endExtent), since that will take an extra
866 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
867 // us overshooting, we brute force the loaded table inside the already existing viewport.
868 const qreal h = qMin(a: viewportRect.bottom(), b: q->contentHeight() + endExtent.height());
869 if (loadedTableOuterRect.bottom() < h) {
870 const qreal diff = loadedTableOuterRect.bottom() - h;
871 loadedTableOuterRect.moveBottom(pos: loadedTableOuterRect.bottom() - diff);
872 loadedTableInnerRect.moveBottom(pos: loadedTableInnerRect.bottom() - diff);
873 tableMovedVertically = true;
874 }
875 }
876 endExtent.rheight() = loadedTableOuterRect.bottom() - q->contentHeight();
877 vData.markExtentsDirty();
878 } else if (loadedTableOuterRect.bottom() >= q->contentHeight() + endExtent.height() - cellSpacing.height()) {
879 // The bottom-most row is outside the end of the content view, and we
880 // still have more visible rows in the model. This can happen if the application
881 // has set a fixed content height.
882 const int rowsRemaining = tableSize.height() - nextBottomRow;
883 const qreal remainingRowHeigts = rowsRemaining * averageEdgeSize.height();
884 const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
885 const qreal estimatedRemainingHeight = remainingRowHeigts + remainingSpacing;
886 const qreal pixelsOutsideContentHeight = loadedTableOuterRect.bottom() - q->contentHeight();
887 endExtent.rheight() = pixelsOutsideContentHeight + estimatedRemainingHeight;
888 vData.markExtentsDirty();
889 }
890
891 if (tableMovedHorizontally || tableMovedVertically) {
892 qCDebug(lcTableViewDelegateLifecycle) << "move table to" << loadedTableOuterRect;
893
894 // relayoutTableItems() will take care of moving the existing
895 // delegate items into the new loadedTableOuterRect.
896 relayoutTableItems();
897
898 // Inform the sync children that they need to rebuild to stay in sync
899 for (auto syncChild : qAsConst(t&: syncChildren)) {
900 auto syncChild_d = syncChild->d_func();
901 syncChild_d->scheduledRebuildOptions |= RebuildOption::ViewportOnly;
902 if (tableMovedHorizontally)
903 syncChild_d->scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn;
904 if (tableMovedVertically)
905 syncChild_d->scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow;
906 }
907 }
908
909 if (hData.minExtentDirty || vData.minExtentDirty) {
910 qCDebug(lcTableViewDelegateLifecycle) << "move origin and endExtent to:" << origin << endExtent;
911 // updateBeginningEnd() will let the new extents take effect. This will also change the
912 // visualArea of the flickable, which again will cause any attached scrollbars to adjust
913 // the position of the handle. Note the latter will cause the viewport to move once more.
914 updateBeginningEnd();
915 }
916}
917
918void QQuickTableViewPrivate::updateAverageColumnWidth()
919{
920 if (explicitContentWidth.isValid()) {
921 const qreal accColumnSpacing = (tableSize.width() - 1) * cellSpacing.width();
922 averageEdgeSize.setWidth((explicitContentWidth - accColumnSpacing) / tableSize.width());
923 } else {
924 const qreal accColumnSpacing = (loadedColumns.count() - 1) * cellSpacing.width();
925 averageEdgeSize.setWidth((loadedTableOuterRect.width() - accColumnSpacing) / loadedColumns.count());
926 }
927}
928
929void QQuickTableViewPrivate::updateAverageRowHeight()
930{
931 if (explicitContentHeight.isValid()) {
932 const qreal accRowSpacing = (tableSize.height() - 1) * cellSpacing.height();
933 averageEdgeSize.setHeight((explicitContentHeight - accRowSpacing) / tableSize.height());
934 } else {
935 const qreal accRowSpacing = (loadedRows.count() - 1) * cellSpacing.height();
936 averageEdgeSize.setHeight((loadedTableOuterRect.height() - accRowSpacing) / loadedRows.count());
937 }
938}
939
940void QQuickTableViewPrivate::syncLoadedTableRectFromLoadedTable()
941{
942 const QPoint topLeft = QPoint(leftColumn(), topRow());
943 const QPoint bottomRight = QPoint(rightColumn(), bottomRow());
944 QRectF topLeftRect = loadedTableItem(cell: topLeft)->geometry();
945 QRectF bottomRightRect = loadedTableItem(cell: bottomRight)->geometry();
946 loadedTableOuterRect = QRectF(topLeftRect.topLeft(), bottomRightRect.bottomRight());
947 loadedTableInnerRect = QRectF(topLeftRect.bottomRight(), bottomRightRect.topLeft());
948}
949
950QQuickTableViewPrivate::RebuildOptions QQuickTableViewPrivate::checkForVisibilityChanges()
951{
952 // This function will check if there are any visibility changes among
953 // the _already loaded_ rows and columns. Note that there can be rows
954 // and columns to the bottom or right that was not loaded, but should
955 // now become visible (in case there is free space around the table).
956 if (loadedItems.isEmpty()) {
957 // Report no changes
958 return RebuildOption::None;
959 }
960
961 RebuildOptions rebuildOptions = RebuildOption::None;
962
963 if (loadedTableOuterRect.x() == origin.x() && leftColumn() != 0) {
964 // Since the left column is at the origin of the viewport, but still not the first
965 // column in the model, we need to calculate a new left column since there might be
966 // columns in front of it that used to be hidden, but should now be visible (QTBUG-93264).
967 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
968 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftColumn);
969 } else {
970 // Go through all loaded columns from first to last, find the columns that used
971 // to be hidden and not loaded, and check if they should become visible
972 // (and vice versa). If there is a change, we need to rebuild.
973 for (int column = leftColumn(); column <= rightColumn(); ++column) {
974 const bool wasVisibleFromBefore = loadedColumns.contains(key: column);
975 const bool isVisibleNow = !qFuzzyIsNull(d: getColumnWidth(column));
976 if (wasVisibleFromBefore == isVisibleNow)
977 continue;
978
979 // A column changed visibility. This means that it should
980 // either be loaded or unloaded. So we need a rebuild.
981 qCDebug(lcTableViewDelegateLifecycle) << "Column" << column << "changed visibility to" << isVisibleNow;
982 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
983 if (column == leftColumn()) {
984 // The first loaded column should now be hidden. This means that we
985 // need to calculate which column should now be first instead.
986 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftColumn);
987 }
988 break;
989 }
990 }
991
992 if (loadedTableOuterRect.y() == origin.y() && topRow() != 0) {
993 // Since the top row is at the origin of the viewport, but still not the first
994 // row in the model, we need to calculate a new top row since there might be
995 // rows in front of it that used to be hidden, but should now be visible (QTBUG-93264).
996 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
997 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftRow);
998 } else {
999 // Go through all loaded rows from first to last, find the rows that used
1000 // to be hidden and not loaded, and check if they should become visible
1001 // (and vice versa). If there is a change, we need to rebuild.
1002 for (int row = topRow(); row <= bottomRow(); ++row) {
1003 const bool wasVisibleFromBefore = loadedRows.contains(key: row);
1004 const bool isVisibleNow = !qFuzzyIsNull(d: getRowHeight(row));
1005 if (wasVisibleFromBefore == isVisibleNow)
1006 continue;
1007
1008 // A row changed visibility. This means that it should
1009 // either be loaded or unloaded. So we need a rebuild.
1010 qCDebug(lcTableViewDelegateLifecycle) << "Row" << row << "changed visibility to" << isVisibleNow;
1011 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
1012 if (row == topRow())
1013 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftRow);
1014 break;
1015 }
1016 }
1017
1018 return rebuildOptions;
1019}
1020
1021void QQuickTableViewPrivate::forceLayout()
1022{
1023 clearEdgeSizeCache();
1024 RebuildOptions rebuildOptions = RebuildOption::None;
1025
1026 const QSize actualTableSize = calculateTableSize();
1027 if (tableSize != actualTableSize) {
1028 // This can happen if the app is calling forceLayout while
1029 // the model is updated, but before we're notified about it.
1030 rebuildOptions = RebuildOption::All;
1031 } else {
1032 // Resizing a column (or row) can result in the table going from being
1033 // e.g completely inside the viewport to go outside. And in the latter
1034 // case, the user needs to be able to scroll the viewport, also if
1035 // flags such as Flickable.StopAtBounds is in use. So we need to
1036 // update contentWidth/Height to support that case.
1037 rebuildOptions = RebuildOption::LayoutOnly
1038 | RebuildOption::CalculateNewContentWidth
1039 | RebuildOption::CalculateNewContentHeight
1040 | checkForVisibilityChanges();
1041 }
1042
1043 scheduleRebuildTable(options: rebuildOptions);
1044
1045 auto rootView = rootSyncView();
1046 const bool updated = rootView->d_func()->updateTableRecursive();
1047 if (!updated) {
1048 qWarning() << "TableView::forceLayout(): Cannot do an immediate re-layout during an ongoing layout!";
1049 rootView->polish();
1050 }
1051}
1052
1053void QQuickTableViewPrivate::syncLoadedTableFromLoadRequest()
1054{
1055 if (loadRequest.edge() == Qt::Edge(0)) {
1056 // No edge means we're loading the top-left item
1057 loadedColumns.insert(key: loadRequest.column(), value: 0);
1058 loadedRows.insert(key: loadRequest.row(), value: 0);
1059 return;
1060 }
1061
1062 switch (loadRequest.edge()) {
1063 case Qt::LeftEdge:
1064 case Qt::RightEdge:
1065 loadedColumns.insert(key: loadRequest.column(), value: 0);
1066 break;
1067 case Qt::TopEdge:
1068 case Qt::BottomEdge:
1069 loadedRows.insert(key: loadRequest.row(), value: 0);
1070 break;
1071 }
1072}
1073
1074FxTableItem *QQuickTableViewPrivate::loadedTableItem(const QPoint &cell) const
1075{
1076 const int modelIndex = modelIndexAtCell(cell);
1077 Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell);
1078 return loadedItems.value(key: modelIndex);
1079}
1080
1081FxTableItem *QQuickTableViewPrivate::createFxTableItem(const QPoint &cell, QQmlIncubator::IncubationMode incubationMode)
1082{
1083 Q_Q(QQuickTableView);
1084
1085 bool ownItem = false;
1086 int modelIndex = modelIndexAtCell(cell);
1087
1088 QObject* object = model->object(index: modelIndex, incubationMode);
1089 if (!object) {
1090 if (model->incubationStatus(index: modelIndex) == QQmlIncubator::Loading) {
1091 // Item is incubating. Return nullptr for now, and let the table call this
1092 // function again once we get a callback to itemCreatedCallback().
1093 return nullptr;
1094 }
1095
1096 qWarning() << "TableView: failed loading index:" << modelIndex;
1097 object = new QQuickItem();
1098 ownItem = true;
1099 }
1100
1101 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
1102 if (!item) {
1103 // The model could not provide an QQuickItem for the
1104 // given index, so we create a placeholder instead.
1105 qWarning() << "TableView: delegate is not an item:" << modelIndex;
1106 model->release(object);
1107 item = new QQuickItem();
1108 ownItem = true;
1109 } else {
1110 QQuickAnchors *anchors = QQuickItemPrivate::get(item)->_anchors;
1111 if (anchors && anchors->activeDirections())
1112 qmlWarning(me: item) << "TableView: detected anchors on delegate with index: " << modelIndex
1113 << ". Use implicitWidth and implicitHeight instead.";
1114 }
1115
1116 if (ownItem) {
1117 // Parent item is normally set early on from initItemCallback (to
1118 // allow bindings to the parent property). But if we created the item
1119 // within this function, we need to set it explicit.
1120 item->setImplicitWidth(kDefaultColumnWidth);
1121 item->setImplicitHeight(kDefaultRowHeight);
1122 item->setParentItem(q->contentItem());
1123 }
1124 Q_TABLEVIEW_ASSERT(item->parentItem() == q->contentItem(), item->parentItem());
1125
1126 FxTableItem *fxTableItem = new FxTableItem(item, q, ownItem);
1127 fxTableItem->setVisible(false);
1128 fxTableItem->cell = cell;
1129 fxTableItem->index = modelIndex;
1130 return fxTableItem;
1131}
1132
1133FxTableItem *QQuickTableViewPrivate::loadFxTableItem(const QPoint &cell, QQmlIncubator::IncubationMode incubationMode)
1134{
1135#ifdef QT_DEBUG
1136 // Since TableView needs to work flawlessly when e.g incubating inside an async
1137 // loader, being able to override all loading to async while debugging can be helpful.
1138 static const bool forcedAsync = forcedIncubationMode == QLatin1String("async");
1139 if (forcedAsync)
1140 incubationMode = QQmlIncubator::Asynchronous;
1141#endif
1142
1143 // Note that even if incubation mode is asynchronous, the item might
1144 // be ready immediately since the model has a cache of items.
1145 QBoolBlocker guard(blockItemCreatedCallback);
1146 auto item = createFxTableItem(cell, incubationMode);
1147 qCDebug(lcTableViewDelegateLifecycle) << cell << "ready?" << bool(item);
1148 return item;
1149}
1150
1151void QQuickTableViewPrivate::releaseLoadedItems(QQmlTableInstanceModel::ReusableFlag reusableFlag) {
1152 // Make a copy and clear the list of items first to avoid destroyed
1153 // items being accessed during the loop (QTBUG-61294)
1154 auto const tmpList = loadedItems;
1155 loadedItems.clear();
1156 for (FxTableItem *item : tmpList)
1157 releaseItem(fxTableItem: item, reusableFlag);
1158}
1159
1160void QQuickTableViewPrivate::releaseItem(FxTableItem *fxTableItem, QQmlTableInstanceModel::ReusableFlag reusableFlag)
1161{
1162 Q_Q(QQuickTableView);
1163 // Note that fxTableItem->item might already have been destroyed, in case
1164 // the item is owned by the QML context rather than the model (e.g ObjectModel etc).
1165 auto item = fxTableItem->item;
1166
1167 if (fxTableItem->ownItem) {
1168 Q_TABLEVIEW_ASSERT(item, fxTableItem->index);
1169 delete item;
1170 } else if (item) {
1171 auto releaseFlag = model->release(object: item, reusableFlag);
1172 if (releaseFlag == QQmlInstanceModel::Pooled) {
1173 fxTableItem->setVisible(false);
1174
1175 // If the item (or a descendant) has focus, remove it, so
1176 // that the item doesn't enter with focus when it's reused.
1177 if (QQuickWindow *window = item->window()) {
1178 const auto focusItem = qobject_cast<QQuickItem *>(object: window->focusObject());
1179 if (focusItem) {
1180 const bool hasFocus = item == focusItem || item->isAncestorOf(child: focusItem);
1181 if (hasFocus) {
1182 const auto focusChild = QQuickItemPrivate::get(item: q)->subFocusItem;
1183 QQuickWindowPrivate::get(c: window)->clearFocusInScope(scope: q, item: focusChild, reason: Qt::OtherFocusReason);
1184 }
1185 }
1186 }
1187 }
1188 }
1189
1190 delete fxTableItem;
1191}
1192
1193void QQuickTableViewPrivate::unloadItem(const QPoint &cell)
1194{
1195 const int modelIndex = modelIndexAtCell(cell);
1196 Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell);
1197 releaseItem(fxTableItem: loadedItems.take(key: modelIndex), reusableFlag);
1198}
1199
1200bool QQuickTableViewPrivate::canLoadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const
1201{
1202 switch (tableEdge) {
1203 case Qt::LeftEdge:
1204 return loadedTableOuterRect.left() > fillRect.left() + cellSpacing.width();
1205 case Qt::RightEdge:
1206 return loadedTableOuterRect.right() < fillRect.right() - cellSpacing.width();
1207 case Qt::TopEdge:
1208 return loadedTableOuterRect.top() > fillRect.top() + cellSpacing.height();
1209 case Qt::BottomEdge:
1210 return loadedTableOuterRect.bottom() < fillRect.bottom() - cellSpacing.height();
1211 }
1212
1213 return false;
1214}
1215
1216bool QQuickTableViewPrivate::canUnloadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const
1217{
1218 // Note: if there is only one row or column left, we cannot unload, since
1219 // they are needed as anchor point for further layouting.
1220 switch (tableEdge) {
1221 case Qt::LeftEdge:
1222 if (loadedColumns.count() <= 1)
1223 return false;
1224 return loadedTableInnerRect.left() <= fillRect.left();
1225 case Qt::RightEdge:
1226 if (loadedColumns.count() <= 1)
1227 return false;
1228 return loadedTableInnerRect.right() >= fillRect.right();
1229 case Qt::TopEdge:
1230 if (loadedRows.count() <= 1)
1231 return false;
1232 return loadedTableInnerRect.top() <= fillRect.top();
1233 case Qt::BottomEdge:
1234 if (loadedRows.count() <= 1)
1235 return false;
1236 return loadedTableInnerRect.bottom() >= fillRect.bottom();
1237 }
1238 Q_TABLEVIEW_UNREACHABLE(tableEdge);
1239 return false;
1240}
1241
1242Qt::Edge QQuickTableViewPrivate::nextEdgeToLoad(const QRectF rect)
1243{
1244 for (Qt::Edge edge : allTableEdges) {
1245 if (!canLoadTableEdge(tableEdge: edge, fillRect: rect))
1246 continue;
1247 const int nextIndex = nextVisibleEdgeIndexAroundLoadedTable(edge);
1248 if (nextIndex == kEdgeIndexAtEnd)
1249 continue;
1250 return edge;
1251 }
1252
1253 return Qt::Edge(0);
1254}
1255
1256Qt::Edge QQuickTableViewPrivate::nextEdgeToUnload(const QRectF rect)
1257{
1258 for (Qt::Edge edge : allTableEdges) {
1259 if (canUnloadTableEdge(tableEdge: edge, fillRect: rect))
1260 return edge;
1261 }
1262 return Qt::Edge(0);
1263}
1264
1265qreal QQuickTableViewPrivate::cellWidth(const QPoint& cell)
1266{
1267 // Using an items width directly is not an option, since we change
1268 // it during layout (which would also cause problems when recycling items).
1269 auto const cellItem = loadedTableItem(cell)->item;
1270 return cellItem->implicitWidth();
1271}
1272
1273qreal QQuickTableViewPrivate::cellHeight(const QPoint& cell)
1274{
1275 // Using an items height directly is not an option, since we change
1276 // it during layout (which would also cause problems when recycling items).
1277 auto const cellItem = loadedTableItem(cell)->item;
1278 return cellItem->implicitHeight();
1279}
1280
1281qreal QQuickTableViewPrivate::sizeHintForColumn(int column)
1282{
1283 // Find the widest cell in the column, and return its width
1284 qreal columnWidth = 0;
1285 for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1286 const int row = r.key();
1287 columnWidth = qMax(a: columnWidth, b: cellWidth(cell: QPoint(column, row)));
1288 }
1289
1290 return columnWidth;
1291}
1292
1293qreal QQuickTableViewPrivate::sizeHintForRow(int row)
1294{
1295 // Find the highest cell in the row, and return its height
1296 qreal rowHeight = 0;
1297 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1298 const int column = c.key();
1299 rowHeight = qMax(a: rowHeight, b: cellHeight(cell: QPoint(column, row)));
1300 }
1301
1302 return rowHeight;
1303}
1304
1305void QQuickTableViewPrivate::updateTableSize()
1306{
1307 // tableSize is the same as row and column count, and will always
1308 // be the same as the number of rows and columns in the model.
1309 Q_Q(QQuickTableView);
1310
1311 const QSize prevTableSize = tableSize;
1312 tableSize = calculateTableSize();
1313
1314 if (prevTableSize.width() != tableSize.width())
1315 emit q->columnsChanged();
1316 if (prevTableSize.height() != tableSize.height())
1317 emit q->rowsChanged();
1318}
1319
1320QSize QQuickTableViewPrivate::calculateTableSize()
1321{
1322 QSize size(0, 0);
1323 if (tableModel)
1324 size = QSize(tableModel->columns(), tableModel->rows());
1325 else if (model)
1326 size = QSize(1, model->count());
1327
1328 return isTransposed ? size.transposed() : size;
1329}
1330
1331qreal QQuickTableViewPrivate::getColumnLayoutWidth(int column)
1332{
1333 // Return the column width specified by the application, or go
1334 // through the loaded items and calculate it as a fallback. For
1335 // layouting, the width can never be zero (or negative), as this
1336 // can lead us to be stuck in an infinite loop trying to load and
1337 // fill out the empty viewport space with empty columns.
1338 const qreal explicitColumnWidth = getColumnWidth(column);
1339 if (explicitColumnWidth >= 0)
1340 return explicitColumnWidth;
1341
1342 if (syncHorizontally) {
1343 if (syncView->d_func()->loadedColumns.contains(key: column))
1344 return syncView->d_func()->getColumnLayoutWidth(column);
1345 }
1346
1347 // Iterate over the currently visible items in the column. The downside
1348 // of doing that, is that the column width will then only be based on the implicit
1349 // width of the currently loaded items (which can be different depending on which
1350 // row you're at when the column is flicked in). The upshot is that you don't have to
1351 // bother setting columnWidthProvider for small tables, or if the implicit width doesn't vary.
1352 qreal columnWidth = sizeHintForColumn(column);
1353
1354 if (qIsNaN(d: columnWidth) || columnWidth <= 0) {
1355 if (!layoutWarningIssued) {
1356 layoutWarningIssued = true;
1357 qmlWarning(me: q_func()) << "the delegate's implicitWidth needs to be greater than zero";
1358 }
1359 columnWidth = kDefaultColumnWidth;
1360 }
1361
1362 return columnWidth;
1363}
1364
1365qreal QQuickTableViewPrivate::getRowLayoutHeight(int row)
1366{
1367 // Return the row height specified by the application, or go
1368 // through the loaded items and calculate it as a fallback. For
1369 // layouting, the height can never be zero (or negative), as this
1370 // can lead us to be stuck in an infinite loop trying to load and
1371 // fill out the empty viewport space with empty rows.
1372 const qreal explicitRowHeight = getRowHeight(row);
1373 if (explicitRowHeight >= 0)
1374 return explicitRowHeight;
1375
1376 if (syncVertically) {
1377 if (syncView->d_func()->loadedRows.contains(key: row))
1378 return syncView->d_func()->getRowLayoutHeight(row);
1379 }
1380
1381 // Iterate over the currently visible items in the row. The downside
1382 // of doing that, is that the row height will then only be based on the implicit
1383 // height of the currently loaded items (which can be different depending on which
1384 // column you're at when the row is flicked in). The upshot is that you don't have to
1385 // bother setting rowHeightProvider for small tables, or if the implicit height doesn't vary.
1386 qreal rowHeight = sizeHintForRow(row);
1387
1388 if (qIsNaN(d: rowHeight) || rowHeight <= 0) {
1389 if (!layoutWarningIssued) {
1390 layoutWarningIssued = true;
1391 qmlWarning(me: q_func()) << "the delegate's implicitHeight needs to be greater than zero";
1392 }
1393 rowHeight = kDefaultRowHeight;
1394 }
1395
1396 return rowHeight;
1397}
1398
1399qreal QQuickTableViewPrivate::getColumnWidth(int column)
1400{
1401 // Return the width of the given column, if explicitly set. Return 0 if the column
1402 // is hidden, and -1 if the width is not set (which means that the width should
1403 // instead be calculated from the implicit size of the delegate items. This function
1404 // can be overridden by e.g HeaderView to provide the column widths by other means.
1405 const int noExplicitColumnWidth = -1;
1406
1407 if (cachedColumnWidth.startIndex == column)
1408 return cachedColumnWidth.size;
1409
1410 if (syncHorizontally)
1411 return syncView->d_func()->getColumnWidth(column);
1412
1413 auto cw = columnWidths.size(section: column);
1414 if (cw >= 0)
1415 return cw;
1416
1417 if (columnWidthProvider.isUndefined())
1418 return noExplicitColumnWidth;
1419
1420 qreal columnWidth = noExplicitColumnWidth;
1421
1422 if (columnWidthProvider.isCallable()) {
1423 auto const columnAsArgument = QJSValueList() << QJSValue(column);
1424 columnWidth = columnWidthProvider.call(args: columnAsArgument).toNumber();
1425 if (qIsNaN(d: columnWidth) || columnWidth < 0)
1426 columnWidth = noExplicitColumnWidth;
1427 } else {
1428 if (!layoutWarningIssued) {
1429 layoutWarningIssued = true;
1430 qmlWarning(me: q_func()) << "columnWidthProvider doesn't contain a function";
1431 }
1432 columnWidth = noExplicitColumnWidth;
1433 }
1434
1435 cachedColumnWidth.startIndex = column;
1436 cachedColumnWidth.size = columnWidth;
1437 return columnWidth;
1438}
1439
1440qreal QQuickTableViewPrivate::getRowHeight(int row)
1441{
1442 // Return the height of the given row, if explicitly set. Return 0 if the row
1443 // is hidden, and -1 if the height is not set (which means that the height should
1444 // instead be calculated from the implicit size of the delegate items. This function
1445 // can be overridden by e.g HeaderView to provide the row heights by other means.
1446 const int noExplicitRowHeight = -1;
1447
1448 if (cachedRowHeight.startIndex == row)
1449 return cachedRowHeight.size;
1450
1451 if (syncVertically)
1452 return syncView->d_func()->getRowHeight(row);
1453
1454 auto rh = rowHeights.size(section: row);
1455 if (rh >= 0)
1456 return rh;
1457
1458 if (rowHeightProvider.isUndefined())
1459 return noExplicitRowHeight;
1460
1461 qreal rowHeight = noExplicitRowHeight;
1462
1463 if (rowHeightProvider.isCallable()) {
1464 auto const rowAsArgument = QJSValueList() << QJSValue(row);
1465 rowHeight = rowHeightProvider.call(args: rowAsArgument).toNumber();
1466 if (qIsNaN(d: rowHeight) || rowHeight < 0)
1467 rowHeight = noExplicitRowHeight;
1468 } else {
1469 if (!layoutWarningIssued) {
1470 layoutWarningIssued = true;
1471 qmlWarning(me: q_func()) << "rowHeightProvider doesn't contain a function";
1472 }
1473 rowHeight = noExplicitRowHeight;
1474 }
1475
1476 cachedRowHeight.startIndex = row;
1477 cachedRowHeight.size = rowHeight;
1478 return rowHeight;
1479}
1480
1481bool QQuickTableViewPrivate::isColumnHidden(int column)
1482{
1483 // A column is hidden if the width is explicit set to zero (either by
1484 // using a columnWidthProvider, or by overriding getColumnWidth()).
1485 return qFuzzyIsNull(d: getColumnWidth(column));
1486}
1487
1488bool QQuickTableViewPrivate::isRowHidden(int row)
1489{
1490 // A row is hidden if the height is explicit set to zero (either by
1491 // using a rowHeightProvider, or by overriding getRowHeight()).
1492 return qFuzzyIsNull(d: getRowHeight(row));
1493}
1494
1495void QQuickTableViewPrivate::relayoutTableItems()
1496{
1497 qCDebug(lcTableViewDelegateLifecycle);
1498
1499 qreal nextColumnX = loadedTableOuterRect.x();
1500 qreal nextRowY = loadedTableOuterRect.y();
1501
1502 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1503 const int column = c.key();
1504 // Adjust the geometry of all cells in the current column
1505 const qreal width = getColumnLayoutWidth(column);
1506
1507 for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1508 const int row = r.key();
1509 auto item = loadedTableItem(cell: QPoint(column, row));
1510 QRectF geometry = item->geometry();
1511 geometry.moveLeft(pos: nextColumnX);
1512 geometry.setWidth(width);
1513 item->setGeometry(geometry);
1514 }
1515
1516 if (width > 0)
1517 nextColumnX += width + cellSpacing.width();
1518 }
1519
1520 for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1521 const int row = r.key();
1522 // Adjust the geometry of all cells in the current row
1523 const qreal height = getRowLayoutHeight(row);
1524
1525 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1526 const int column = c.key();
1527 auto item = loadedTableItem(cell: QPoint(column, row));
1528 QRectF geometry = item->geometry();
1529 geometry.moveTop(pos: nextRowY);
1530 geometry.setHeight(height);
1531 item->setGeometry(geometry);
1532 }
1533
1534 if (height > 0)
1535 nextRowY += height + cellSpacing.height();
1536 }
1537
1538 if (Q_UNLIKELY(lcTableViewDelegateLifecycle().isDebugEnabled())) {
1539 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1540 const int column = c.key();
1541 for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1542 const int row = r.key();
1543 QPoint cell = QPoint(column, row);
1544 qCDebug(lcTableViewDelegateLifecycle()) << "relayout item:" << cell << loadedTableItem(cell)->geometry();
1545 }
1546 }
1547 }
1548}
1549
1550void QQuickTableViewPrivate::layoutVerticalEdge(Qt::Edge tableEdge)
1551{
1552 int columnThatNeedsLayout;
1553 int neighbourColumn;
1554 qreal columnX;
1555 qreal columnWidth;
1556
1557 if (tableEdge == Qt::LeftEdge) {
1558 columnThatNeedsLayout = leftColumn();
1559 neighbourColumn = loadedColumns.keys().value(i: 1);
1560 columnWidth = getColumnLayoutWidth(column: columnThatNeedsLayout);
1561 const auto neighbourItem = loadedTableItem(cell: QPoint(neighbourColumn, topRow()));
1562 columnX = neighbourItem->geometry().left() - cellSpacing.width() - columnWidth;
1563 } else {
1564 columnThatNeedsLayout = rightColumn();
1565 neighbourColumn = loadedColumns.keys().value(i: loadedColumns.count() - 2);
1566 columnWidth = getColumnLayoutWidth(column: columnThatNeedsLayout);
1567 const auto neighbourItem = loadedTableItem(cell: QPoint(neighbourColumn, topRow()));
1568 columnX = neighbourItem->geometry().right() + cellSpacing.width();
1569 }
1570
1571 for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r) {
1572 const int row = r.key();
1573 auto fxTableItem = loadedTableItem(cell: QPoint(columnThatNeedsLayout, row));
1574 auto const neighbourItem = loadedTableItem(cell: QPoint(neighbourColumn, row));
1575 const qreal rowY = neighbourItem->geometry().y();
1576 const qreal rowHeight = neighbourItem->geometry().height();
1577
1578 fxTableItem->setGeometry(QRectF(columnX, rowY, columnWidth, rowHeight));
1579 fxTableItem->setVisible(true);
1580
1581 qCDebug(lcTableViewDelegateLifecycle()) << "layout item:" << QPoint(columnThatNeedsLayout, row) << fxTableItem->geometry();
1582 }
1583}
1584
1585void QQuickTableViewPrivate::layoutHorizontalEdge(Qt::Edge tableEdge)
1586{
1587 int rowThatNeedsLayout;
1588 int neighbourRow;
1589
1590 if (tableEdge == Qt::TopEdge) {
1591 rowThatNeedsLayout = topRow();
1592 neighbourRow = loadedRows.keys().value(i: 1);
1593 } else {
1594 rowThatNeedsLayout = bottomRow();
1595 neighbourRow = loadedRows.keys().value(i: loadedRows.count() - 2);
1596 }
1597
1598 // Set the width first, since text items in QtQuick will calculate
1599 // implicitHeight based on the text items width.
1600 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1601 const int column = c.key();
1602 auto fxTableItem = loadedTableItem(cell: QPoint(column, rowThatNeedsLayout));
1603 auto const neighbourItem = loadedTableItem(cell: QPoint(column, neighbourRow));
1604 const qreal columnX = neighbourItem->geometry().x();
1605 const qreal columnWidth = neighbourItem->geometry().width();
1606 fxTableItem->item->setX(columnX);
1607 fxTableItem->item->setWidth(columnWidth);
1608 }
1609
1610 qreal rowY;
1611 qreal rowHeight;
1612 if (tableEdge == Qt::TopEdge) {
1613 rowHeight = getRowLayoutHeight(row: rowThatNeedsLayout);
1614 const auto neighbourItem = loadedTableItem(cell: QPoint(leftColumn(), neighbourRow));
1615 rowY = neighbourItem->geometry().top() - cellSpacing.height() - rowHeight;
1616 } else {
1617 rowHeight = getRowLayoutHeight(row: rowThatNeedsLayout);
1618 const auto neighbourItem = loadedTableItem(cell: QPoint(leftColumn(), neighbourRow));
1619 rowY = neighbourItem->geometry().bottom() + cellSpacing.height();
1620 }
1621
1622 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c) {
1623 const int column = c.key();
1624 auto fxTableItem = loadedTableItem(cell: QPoint(column, rowThatNeedsLayout));
1625 fxTableItem->item->setY(rowY);
1626 fxTableItem->item->setHeight(rowHeight);
1627 fxTableItem->setVisible(true);
1628
1629 qCDebug(lcTableViewDelegateLifecycle()) << "layout item:" << QPoint(column, rowThatNeedsLayout) << fxTableItem->geometry();
1630 }
1631}
1632
1633void QQuickTableViewPrivate::layoutTopLeftItem()
1634{
1635 const QPoint cell(loadRequest.column(), loadRequest.row());
1636 auto topLeftItem = loadedTableItem(cell);
1637 auto item = topLeftItem->item;
1638
1639 item->setPosition(loadRequest.startPosition());
1640 item->setSize(QSizeF(getColumnLayoutWidth(column: cell.x()), getRowLayoutHeight(row: cell.y())));
1641 topLeftItem->setVisible(true);
1642 qCDebug(lcTableViewDelegateLifecycle) << "geometry:" << topLeftItem->geometry();
1643}
1644
1645void QQuickTableViewPrivate::layoutTableEdgeFromLoadRequest()
1646{
1647 if (loadRequest.edge() == Qt::Edge(0)) {
1648 // No edge means we're loading the top-left item
1649 layoutTopLeftItem();
1650 return;
1651 }
1652
1653 switch (loadRequest.edge()) {
1654 case Qt::LeftEdge:
1655 case Qt::RightEdge:
1656 layoutVerticalEdge(tableEdge: loadRequest.edge());
1657 break;
1658 case Qt::TopEdge:
1659 case Qt::BottomEdge:
1660 layoutHorizontalEdge(tableEdge: loadRequest.edge());
1661 break;
1662 }
1663}
1664
1665void QQuickTableViewPrivate::processLoadRequest()
1666{
1667 Q_TABLEVIEW_ASSERT(loadRequest.isActive(), "");
1668
1669 while (loadRequest.hasCurrentCell()) {
1670 QPoint cell = loadRequest.currentCell();
1671 FxTableItem *fxTableItem = loadFxTableItem(cell, incubationMode: loadRequest.incubationMode());
1672
1673 if (!fxTableItem) {
1674 // Requested item is not yet ready. Just leave, and wait for this
1675 // function to be called again when the item is ready.
1676 return;
1677 }
1678
1679 loadedItems.insert(key: modelIndexAtCell(cell), value: fxTableItem);
1680 loadRequest.moveToNextCell();
1681 }
1682
1683 qCDebug(lcTableViewDelegateLifecycle()) << "all items loaded!";
1684
1685 syncLoadedTableFromLoadRequest();
1686 layoutTableEdgeFromLoadRequest();
1687 syncLoadedTableRectFromLoadedTable();
1688
1689 if (rebuildState == RebuildState::Done) {
1690 // Loading of this edge was not done as a part of a rebuild, but
1691 // instead as an incremental build after e.g a flick.
1692 updateExtents();
1693 drainReusePoolAfterLoadRequest();
1694 }
1695
1696 loadRequest.markAsDone();
1697
1698 qCDebug(lcTableViewDelegateLifecycle()) << "request completed! Table:" << tableLayoutToString();
1699}
1700
1701void QQuickTableViewPrivate::processRebuildTable()
1702{
1703 Q_Q(QQuickTableView);
1704
1705 if (rebuildState == RebuildState::Begin) {
1706 if (Q_UNLIKELY(lcTableViewDelegateLifecycle().isDebugEnabled())) {
1707 qCDebug(lcTableViewDelegateLifecycle()) << "begin rebuild:" << q;
1708 if (rebuildOptions & RebuildOption::All)
1709 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::All, options:" << rebuildOptions;
1710 else if (rebuildOptions & RebuildOption::ViewportOnly)
1711 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::ViewportOnly, options:" << rebuildOptions;
1712 else if (rebuildOptions & RebuildOption::LayoutOnly)
1713 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::LayoutOnly, options:" << rebuildOptions;
1714 else
1715 Q_TABLEVIEW_UNREACHABLE(rebuildOptions);
1716 }
1717 }
1718
1719 moveToNextRebuildState();
1720
1721 if (rebuildState == RebuildState::LoadInitalTable) {
1722 beginRebuildTable();
1723 if (!moveToNextRebuildState())
1724 return;
1725 }
1726
1727 if (rebuildState == RebuildState::VerifyTable) {
1728 if (loadedItems.isEmpty()) {
1729 qCDebug(lcTableViewDelegateLifecycle()) << "no items loaded!";
1730 updateContentWidth();
1731 updateContentHeight();
1732 rebuildState = RebuildState::Done;
1733 } else if (!moveToNextRebuildState()) {
1734 return;
1735 }
1736 }
1737
1738 if (rebuildState == RebuildState::LayoutTable) {
1739 layoutAfterLoadingInitialTable();
1740 if (!moveToNextRebuildState())
1741 return;
1742 }
1743
1744 if (rebuildState == RebuildState::LoadAndUnloadAfterLayout) {
1745 loadAndUnloadVisibleEdges();
1746 if (!moveToNextRebuildState())
1747 return;
1748 }
1749
1750 const bool preload = (rebuildOptions & RebuildOption::All
1751 && reusableFlag == QQmlTableInstanceModel::Reusable);
1752
1753 if (rebuildState == RebuildState::PreloadColumns) {
1754 if (preload && nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::RightEdge) != kEdgeIndexAtEnd)
1755 loadEdge(edge: Qt::RightEdge, incubationMode: QQmlIncubator::AsynchronousIfNested);
1756 if (!moveToNextRebuildState())
1757 return;
1758 }
1759
1760 if (rebuildState == RebuildState::PreloadRows) {
1761 if (preload && nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::BottomEdge) != kEdgeIndexAtEnd)
1762 loadEdge(edge: Qt::BottomEdge, incubationMode: QQmlIncubator::AsynchronousIfNested);
1763 if (!moveToNextRebuildState())
1764 return;
1765 }
1766
1767 if (rebuildState == RebuildState::MovePreloadedItemsToPool) {
1768 while (Qt::Edge edge = nextEdgeToUnload(rect: viewportRect))
1769 unloadEdge(edge);
1770 if (!moveToNextRebuildState())
1771 return;
1772 }
1773
1774 Q_TABLEVIEW_ASSERT(rebuildState == RebuildState::Done, int(rebuildState));
1775 qCDebug(lcTableViewDelegateLifecycle()) << "rebuild complete:" << q;
1776}
1777
1778bool QQuickTableViewPrivate::moveToNextRebuildState()
1779{
1780 if (loadRequest.isActive()) {
1781 // Items are still loading async, which means
1782 // that the current state is not yet done.
1783 return false;
1784 }
1785
1786 if (rebuildState == RebuildState::Begin
1787 && rebuildOptions.testFlag(flag: RebuildOption::LayoutOnly))
1788 rebuildState = RebuildState::LayoutTable;
1789 else
1790 rebuildState = RebuildState(int(rebuildState) + 1);
1791
1792 qCDebug(lcTableViewDelegateLifecycle()) << int(rebuildState);
1793 return true;
1794}
1795
1796void QQuickTableViewPrivate::calculateTopLeft(QPoint &topLeftCell, QPointF &topLeftPos)
1797{
1798 if (tableSize.isEmpty()) {
1799 // There is no cell that can be top left
1800 topLeftCell.rx() = kEdgeIndexAtEnd;
1801 topLeftCell.ry() = kEdgeIndexAtEnd;
1802 return;
1803 }
1804
1805 if (syncHorizontally || syncVertically) {
1806 const auto syncView_d = syncView->d_func();
1807
1808 if (syncView_d->loadedItems.isEmpty()) {
1809 topLeftCell.rx() = 0;
1810 topLeftCell.ry() = 0;
1811 return;
1812 }
1813
1814 // Get sync view top left, and use that as our own top left (if possible)
1815 const QPoint syncViewTopLeftCell(syncView_d->leftColumn(), syncView_d->topRow());
1816 const auto syncViewTopLeftFxItem = syncView_d->loadedTableItem(cell: syncViewTopLeftCell);
1817 const QPointF syncViewTopLeftPos = syncViewTopLeftFxItem->geometry().topLeft();
1818
1819 if (syncHorizontally) {
1820 topLeftCell.rx() = syncViewTopLeftCell.x();
1821 topLeftPos.rx() = syncViewTopLeftPos.x();
1822
1823 if (topLeftCell.x() >= tableSize.width()) {
1824 // Top left is outside our own model.
1825 topLeftCell.rx() = kEdgeIndexAtEnd;
1826 topLeftPos.rx() = kEdgeIndexAtEnd;
1827 }
1828 }
1829
1830 if (syncVertically) {
1831 topLeftCell.ry() = syncViewTopLeftCell.y();
1832 topLeftPos.ry() = syncViewTopLeftPos.y();
1833
1834 if (topLeftCell.y() >= tableSize.height()) {
1835 // Top left is outside our own model.
1836 topLeftCell.ry() = kEdgeIndexAtEnd;
1837 topLeftPos.ry() = kEdgeIndexAtEnd;
1838 }
1839 }
1840
1841 if (syncHorizontally && syncVertically) {
1842 // We have a valid top left, so we're done
1843 return;
1844 }
1845 }
1846
1847 // Since we're not sync-ing both horizontal and vertical, calculate the missing
1848 // dimention(s) ourself. If we rebuild all, we find the first visible top-left
1849 // item starting from cell(0, 0). Otherwise, guesstimate which row or column that
1850 // should be the new top-left given the geometry of the viewport.
1851
1852 if (!syncHorizontally) {
1853 if (rebuildOptions & RebuildOption::All) {
1854 // Find the first visible column from the beginning
1855 topLeftCell.rx() = nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: 0);
1856 if (topLeftCell.x() == kEdgeIndexAtEnd) {
1857 // No visible column found
1858 return;
1859 }
1860 } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftColumn) {
1861 // Guesstimate new top left
1862 const int newColumn = int(viewportRect.x() / (averageEdgeSize.width() + cellSpacing.width()));
1863 topLeftCell.rx() = qBound(min: 0, val: newColumn, max: tableSize.width() - 1);
1864 topLeftPos.rx() = topLeftCell.x() * (averageEdgeSize.width() + cellSpacing.width());
1865 } else {
1866 // Keep the current top left, unless it's outside model
1867 topLeftCell.rx() = qBound(min: 0, val: leftColumn(), max: tableSize.width() - 1);
1868 topLeftPos.rx() = loadedTableOuterRect.topLeft().x();
1869 }
1870 }
1871
1872 if (!syncVertically) {
1873 if (rebuildOptions & RebuildOption::All) {
1874 // Find the first visible row from the beginning
1875 topLeftCell.ry() = nextVisibleEdgeIndex(edge: Qt::BottomEdge, startIndex: 0);
1876 if (topLeftCell.y() == kEdgeIndexAtEnd) {
1877 // No visible row found
1878 return;
1879 }
1880 } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftRow) {
1881 // Guesstimate new top left
1882 const int newRow = int(viewportRect.y() / (averageEdgeSize.height() + cellSpacing.height()));
1883 topLeftCell.ry() = qBound(min: 0, val: newRow, max: tableSize.height() - 1);
1884 topLeftPos.ry() = topLeftCell.y() * (averageEdgeSize.height() + cellSpacing.height());
1885 } else {
1886 // Keep the current top left, unless it's outside model
1887 topLeftCell.ry() = qBound(min: 0, val: topRow(), max: tableSize.height() - 1);
1888 topLeftPos.ry() = loadedTableOuterRect.topLeft().y();
1889 }
1890 }
1891}
1892
1893void QQuickTableViewPrivate::beginRebuildTable()
1894{
1895 updateTableSize();
1896
1897 QPoint topLeft;
1898 QPointF topLeftPos;
1899 calculateTopLeft(topLeftCell&: topLeft, topLeftPos);
1900
1901 if (!loadedItems.isEmpty()) {
1902 if (rebuildOptions & RebuildOption::All)
1903 releaseLoadedItems(reusableFlag: QQmlTableInstanceModel::NotReusable);
1904 else if (rebuildOptions & RebuildOption::ViewportOnly)
1905 releaseLoadedItems(reusableFlag);
1906 }
1907
1908 if (rebuildOptions & RebuildOption::All) {
1909 origin = QPointF(0, 0);
1910 endExtent = QSizeF(0, 0);
1911 hData.markExtentsDirty();
1912 vData.markExtentsDirty();
1913 updateBeginningEnd();
1914 }
1915
1916 loadedColumns.clear();
1917 loadedRows.clear();
1918 loadedTableOuterRect = QRect();
1919 loadedTableInnerRect = QRect();
1920 clearEdgeSizeCache();
1921
1922 if (syncHorizontally) {
1923 setLocalViewportX(syncView->contentX());
1924 viewportRect.moveLeft(pos: syncView->d_func()->viewportRect.left());
1925 }
1926
1927 if (syncVertically) {
1928 setLocalViewportY(syncView->contentY());
1929 viewportRect.moveTop(pos: syncView->d_func()->viewportRect.top());
1930 }
1931
1932 syncViewportRect();
1933
1934 if (!model) {
1935 qCDebug(lcTableViewDelegateLifecycle()) << "no model found, leaving table empty";
1936 return;
1937 }
1938
1939 if (model->count() == 0) {
1940 qCDebug(lcTableViewDelegateLifecycle()) << "empty model found, leaving table empty";
1941 return;
1942 }
1943
1944 if (tableModel && !tableModel->delegate()) {
1945 qCDebug(lcTableViewDelegateLifecycle()) << "no delegate found, leaving table empty";
1946 return;
1947 }
1948
1949 if (topLeft.x() == kEdgeIndexAtEnd || topLeft.y() == kEdgeIndexAtEnd) {
1950 qCDebug(lcTableViewDelegateLifecycle()) << "no visible row or column found, leaving table empty";
1951 return;
1952 }
1953
1954 if (topLeft.x() == kEdgeIndexNotSet || topLeft.y() == kEdgeIndexNotSet) {
1955 qCDebug(lcTableViewDelegateLifecycle()) << "could not resolve top-left item, leaving table empty";
1956 return;
1957 }
1958
1959 // Load top-left item. After loaded, loadItemsInsideRect() will take
1960 // care of filling out the rest of the table.
1961 loadRequest.begin(cell: topLeft, pos: topLeftPos, incubationMode: QQmlIncubator::AsynchronousIfNested);
1962 processLoadRequest();
1963 loadAndUnloadVisibleEdges();
1964}
1965
1966void QQuickTableViewPrivate::layoutAfterLoadingInitialTable()
1967{
1968 clearEdgeSizeCache();
1969 relayoutTableItems();
1970 syncLoadedTableRectFromLoadedTable();
1971
1972 if (rebuildOptions.testFlag(flag: RebuildOption::CalculateNewContentWidth) || allColumnsLoaded()) {
1973 updateAverageColumnWidth();
1974 updateContentWidth();
1975 }
1976
1977 if (rebuildOptions.testFlag(flag: RebuildOption::CalculateNewContentHeight) || allRowsLoaded()) {
1978 updateAverageRowHeight();
1979 updateContentHeight();
1980 }
1981
1982 updateExtents();
1983}
1984
1985void QQuickTableViewPrivate::unloadEdge(Qt::Edge edge)
1986{
1987 qCDebug(lcTableViewDelegateLifecycle) << edge;
1988
1989 switch (edge) {
1990 case Qt::LeftEdge:
1991 case Qt::RightEdge: {
1992 const int column = edge == Qt::LeftEdge ? leftColumn() : rightColumn();
1993 for (auto r = loadedRows.cbegin(); r != loadedRows.cend(); ++r)
1994 unloadItem(cell: QPoint(column, r.key()));
1995 loadedColumns.remove(key: column);
1996 syncLoadedTableRectFromLoadedTable();
1997 break; }
1998 case Qt::TopEdge:
1999 case Qt::BottomEdge: {
2000 const int row = edge == Qt::TopEdge ? topRow() : bottomRow();
2001 for (auto c = loadedColumns.cbegin(); c != loadedColumns.cend(); ++c)
2002 unloadItem(cell: QPoint(c.key(), row));
2003 loadedRows.remove(key: row);
2004 syncLoadedTableRectFromLoadedTable();
2005 break; }
2006 }
2007
2008 qCDebug(lcTableViewDelegateLifecycle) << tableLayoutToString();
2009}
2010
2011void QQuickTableViewPrivate::loadEdge(Qt::Edge edge, QQmlIncubator::IncubationMode incubationMode)
2012{
2013 const int edgeIndex = nextVisibleEdgeIndexAroundLoadedTable(edge);
2014 qCDebug(lcTableViewDelegateLifecycle) << edge << edgeIndex;
2015
2016 const QList<int> visibleCells = edge & (Qt::LeftEdge | Qt::RightEdge)
2017 ? loadedRows.keys() : loadedColumns.keys();
2018 loadRequest.begin(edgeToLoad: edge, edgeIndex, visibleCellsInEdge: visibleCells, incubationMode);
2019 processLoadRequest();
2020}
2021
2022void QQuickTableViewPrivate::loadAndUnloadVisibleEdges()
2023{
2024 // Unload table edges that have been moved outside the visible part of the
2025 // table (including buffer area), and load new edges that has been moved inside.
2026 // Note: an important point is that we always keep the table rectangular
2027 // and without holes to reduce complexity (we never leave the table in
2028 // a half-loaded state, or keep track of multiple patches).
2029 // We load only one edge (row or column) at a time. This is especially
2030 // important when loading into the buffer, since we need to be able to
2031 // cancel the buffering quickly if the user starts to flick, and then
2032 // focus all further loading on the edges that are flicked into view.
2033
2034 if (loadRequest.isActive()) {
2035 // Don't start loading more edges while we're
2036 // already waiting for another one to load.
2037 return;
2038 }
2039
2040 if (loadedItems.isEmpty()) {
2041 // We need at least the top-left item to be loaded before we can
2042 // start loading edges around it. Not having a top-left item at
2043 // this point means that the model is empty (or no delegate).
2044 return;
2045 }
2046
2047 bool tableModified;
2048
2049 do {
2050 tableModified = false;
2051
2052 if (Qt::Edge edge = nextEdgeToUnload(rect: viewportRect)) {
2053 tableModified = true;
2054 unloadEdge(edge);
2055 }
2056
2057 if (Qt::Edge edge = nextEdgeToLoad(rect: viewportRect)) {
2058 tableModified = true;
2059 loadEdge(edge, incubationMode: QQmlIncubator::AsynchronousIfNested);
2060 if (loadRequest.isActive())
2061 return;
2062 }
2063 } while (tableModified);
2064
2065}
2066
2067void QQuickTableViewPrivate::drainReusePoolAfterLoadRequest()
2068{
2069 Q_Q(QQuickTableView);
2070
2071 if (reusableFlag == QQmlTableInstanceModel::NotReusable || !tableModel)
2072 return;
2073
2074 if (!qFuzzyIsNull(d: q->verticalOvershoot()) || !qFuzzyIsNull(d: q->horizontalOvershoot())) {
2075 // Don't drain while we're overshooting, since this will fill up the
2076 // pool, but we expect to reuse them all once the content item moves back.
2077 return;
2078 }
2079
2080 // When loading edges, we don't want to drain the reuse pool too aggressively. Normally,
2081 // all the items in the pool are reused rapidly as the content view is flicked around
2082 // anyway. Even if the table is temporarily flicked to a section that contains fewer
2083 // cells than what used to be (e.g if the flicked-in rows are taller than average), it
2084 // still makes sense to keep all the items in circulation; Chances are, that soon enough,
2085 // thinner rows are flicked back in again (meaning that we can fit more items into the
2086 // view). But at the same time, if a delegate chooser is in use, the pool might contain
2087 // items created from different delegates. And some of those delegates might be used only
2088 // occasionally. So to avoid situations where an item ends up in the pool for too long, we
2089 // call drain after each load request, but with a sufficiently large pool time. (If an item
2090 // in the pool has a large pool time, it means that it hasn't been reused for an equal
2091 // amount of load cycles, and should be released).
2092 //
2093 // We calculate an appropriate pool time by figuring out what the minimum time must be to
2094 // not disturb frequently reused items. Since the number of items in a row might be higher
2095 // than in a column (or vice versa), the minimum pool time should take into account that
2096 // you might be flicking out a single row (filling up the pool), before you continue
2097 // flicking in several new columns (taking them out again, but now in smaller chunks). This
2098 // will increase the number of load cycles items are kept in the pool (poolTime), but still,
2099 // we shouldn't release them, as they are still being reused frequently.
2100 // To get a flexible maxValue (that e.g tolerates rows and columns being flicked
2101 // in with varying sizes, causing some items not to be resued immediately), we multiply the
2102 // value by 2. Note that we also add an extra +1 to the column count, because the number of
2103 // visible columns will fluctuate between +1/-1 while flicking.
2104 const int w = loadedColumns.count();
2105 const int h = loadedRows.count();
2106 const int minTime = int(std::ceil(x: w > h ? qreal(w + 1) / h : qreal(h + 1) / w));
2107 const int maxTime = minTime * 2;
2108 tableModel->drainReusableItemsPool(maxPoolTime: maxTime);
2109}
2110
2111void QQuickTableViewPrivate::scheduleRebuildTable(RebuildOptions options) {
2112 if (!q_func()->isComponentComplete()) {
2113 // We'll rebuild the table once complete anyway
2114 return;
2115 }
2116
2117 scheduledRebuildOptions |= options;
2118 q_func()->polish();
2119}
2120
2121QQuickTableView *QQuickTableViewPrivate::rootSyncView() const
2122{
2123 QQuickTableView *root = const_cast<QQuickTableView *>(q_func());
2124 while (QQuickTableView *view = root->d_func()->syncView)
2125 root = view;
2126 return root;
2127}
2128
2129void QQuickTableViewPrivate::updatePolish()
2130{
2131 // We always start updating from the top of the syncView tree, since
2132 // the layout of a syncView child will depend on the layout of the syncView.
2133 // E.g when a new column is flicked in, the syncView should load and layout
2134 // the column first, before any syncChildren gets a chance to do the same.
2135 Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!");
2136 rootSyncView()->d_func()->updateTableRecursive();
2137}
2138
2139bool QQuickTableViewPrivate::updateTableRecursive()
2140{
2141 if (polishing) {
2142 // We're already updating the Table in this view, so
2143 // we cannot continue. Signal this back by returning false.
2144 // The caller can then choose to call "polish()" instead, to
2145 // do the update later.
2146 return false;
2147 }
2148
2149 const bool updateComplete = updateTable();
2150 if (!updateComplete)
2151 return false;
2152
2153 for (auto syncChild : qAsConst(t&: syncChildren)) {
2154 auto syncChild_d = syncChild->d_func();
2155 syncChild_d->scheduledRebuildOptions |= rebuildOptions;
2156
2157 const bool descendantUpdateComplete = syncChild_d->updateTableRecursive();
2158 if (!descendantUpdateComplete)
2159 return false;
2160 }
2161
2162 rebuildOptions = RebuildOption::None;
2163
2164 return true;
2165}
2166
2167bool QQuickTableViewPrivate::updateTable()
2168{
2169 // Whenever something changes, e.g viewport moves, spacing is set to a
2170 // new value, model changes etc, this function will end up being called. Here
2171 // we check what needs to be done, and load/unload cells accordingly.
2172 // If we cannot complete the update (because we need to wait for an item
2173 // to load async), we return false.
2174
2175 Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!");
2176 QBoolBlocker polishGuard(polishing, true);
2177
2178 if (loadRequest.isActive()) {
2179 // We're currently loading items async to build a new edge in the table. We see the loading
2180 // as an atomic operation, which means that we don't continue doing anything else until all
2181 // items have been received and laid out. Note that updatePolish is then called once more
2182 // after the loadRequest has completed to handle anything that might have occurred in-between.
2183 return false;
2184 }
2185
2186 if (rebuildState != RebuildState::Done) {
2187 processRebuildTable();
2188 return rebuildState == RebuildState::Done;
2189 }
2190
2191 syncWithPendingChanges();
2192
2193 if (rebuildState == RebuildState::Begin) {
2194 processRebuildTable();
2195 return rebuildState == RebuildState::Done;
2196 }
2197
2198 if (loadedItems.isEmpty())
2199 return !loadRequest.isActive();
2200
2201 loadAndUnloadVisibleEdges();
2202
2203 return !loadRequest.isActive();
2204}
2205
2206void QQuickTableViewPrivate::fixup(QQuickFlickablePrivate::AxisData &data, qreal minExtent, qreal maxExtent)
2207{
2208 if (inUpdateContentSize) {
2209 // We update the content size dynamically as we load and unload edges.
2210 // Unfortunately, this also triggers a call to this function. The base
2211 // implementation will do things like start a momentum animation or move
2212 // the content view somewhere else, which causes glitches. This can
2213 // especially happen if flicking on one of the syncView children, which triggers
2214 // an update to our content size. In that case, the base implementation don't know
2215 // that the view is being indirectly dragged, and will therefore do strange things as
2216 // it tries to 'fixup' the geometry. So we use a guard to prevent this from happening.
2217 return;
2218 }
2219
2220 QQuickFlickablePrivate::fixup(data, minExtent, maxExtent);
2221}
2222
2223int QQuickTableViewPrivate::resolveImportVersion()
2224{
2225 const auto data = QQmlData::get(object: q_func());
2226 if (!data || !data->propertyCache)
2227 return 0;
2228
2229 const auto cppMetaObject = data->propertyCache->firstCppMetaObject();
2230 const auto qmlTypeView = QQmlMetaType::qmlType(cppMetaObject);
2231 return qmlTypeView.minorVersion();
2232}
2233
2234void QQuickTableViewPrivate::createWrapperModel()
2235{
2236 Q_Q(QQuickTableView);
2237 // When the assigned model is not an instance model, we create a wrapper
2238 // model (QQmlTableInstanceModel) that keeps a pointer to both the
2239 // assigned model and the assigned delegate. This model will give us a
2240 // common interface to any kind of model (js arrays, QAIM, number etc), and
2241 // help us create delegate instances.
2242 tableModel = new QQmlTableInstanceModel(qmlContext(q));
2243 tableModel->useImportVersion(minorVersion: resolveImportVersion());
2244 model = tableModel;
2245}
2246
2247void QQuickTableViewPrivate::itemCreatedCallback(int modelIndex, QObject*)
2248{
2249 if (blockItemCreatedCallback)
2250 return;
2251
2252 qCDebug(lcTableViewDelegateLifecycle) << "item done loading:"
2253 << cellAtModelIndex(modelIndex);
2254
2255 // Since the item we waited for has finished incubating, we can
2256 // continue with the load request. processLoadRequest will
2257 // ask the model for the requested item once more, which will be
2258 // quick since the model has cached it.
2259 processLoadRequest();
2260 loadAndUnloadVisibleEdges();
2261 updatePolish();
2262}
2263
2264void QQuickTableViewPrivate::initItemCallback(int modelIndex, QObject *object)
2265{
2266 Q_UNUSED(modelIndex);
2267 Q_Q(QQuickTableView);
2268
2269 if (auto item = qmlobject_cast<QQuickItem*>(object)) {
2270 item->setParentItem(q->contentItem());
2271 item->setZ(1);
2272 }
2273
2274 if (auto attached = getAttachedObject(object))
2275 attached->setView(q);
2276}
2277
2278void QQuickTableViewPrivate::itemPooledCallback(int modelIndex, QObject *object)
2279{
2280 Q_UNUSED(modelIndex);
2281
2282 if (auto attached = getAttachedObject(object))
2283 emit attached->pooled();
2284}
2285
2286void QQuickTableViewPrivate::itemReusedCallback(int modelIndex, QObject *object)
2287{
2288 Q_UNUSED(modelIndex);
2289
2290 if (auto attached = getAttachedObject(object))
2291 emit attached->reused();
2292}
2293
2294void QQuickTableViewPrivate::syncWithPendingChanges()
2295{
2296 // The application can change properties like the model or the delegate while
2297 // we're e.g in the middle of e.g loading a new row. Since this will lead to
2298 // unpredicted behavior, and possibly a crash, we need to postpone taking
2299 // such assignments into effect until we're in a state that allows it.
2300
2301 syncViewportRect();
2302 syncModel();
2303 syncDelegate();
2304 syncSyncView();
2305
2306 syncRebuildOptions();
2307}
2308
2309void QQuickTableViewPrivate::syncRebuildOptions()
2310{
2311 if (!scheduledRebuildOptions)
2312 return;
2313
2314 rebuildState = RebuildState::Begin;
2315 rebuildOptions = scheduledRebuildOptions;
2316 scheduledRebuildOptions = RebuildOption::None;
2317
2318 if (loadedItems.isEmpty())
2319 rebuildOptions.setFlag(flag: RebuildOption::All);
2320
2321 // Some options are exclusive:
2322 if (rebuildOptions.testFlag(flag: RebuildOption::All)) {
2323 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly, on: false);
2324 rebuildOptions.setFlag(flag: RebuildOption::LayoutOnly, on: false);
2325 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewContentWidth);
2326 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewContentHeight);
2327 } else if (rebuildOptions.testFlag(flag: RebuildOption::ViewportOnly)) {
2328 rebuildOptions.setFlag(flag: RebuildOption::LayoutOnly, on: false);
2329 }
2330}
2331
2332void QQuickTableViewPrivate::syncDelegate()
2333{
2334 if (!tableModel) {
2335 // Only the tableModel uses the delegate assigned to a
2336 // TableView. DelegateModel has it's own delegate, and
2337 // ObjectModel etc. doesn't use one.
2338 return;
2339 }
2340
2341 if (assignedDelegate != tableModel->delegate())
2342 tableModel->setDelegate(assignedDelegate);
2343}
2344
2345QVariant QQuickTableViewPrivate::modelImpl() const
2346{
2347 return assignedModel;
2348}
2349
2350void QQuickTableViewPrivate::setModelImpl(const QVariant &newModel)
2351{
2352 if (newModel == assignedModel)
2353 return;
2354
2355 assignedModel = newModel;
2356 scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::All);
2357 emit q_func()->modelChanged();
2358}
2359
2360void QQuickTableViewPrivate::syncModel()
2361{
2362 if (modelVariant == assignedModel)
2363 return;
2364
2365 if (model) {
2366 disconnectFromModel();
2367 releaseLoadedItems(reusableFlag: QQmlTableInstanceModel::NotReusable);
2368 }
2369
2370 modelVariant = assignedModel;
2371 QVariant effectiveModelVariant = modelVariant;
2372 if (effectiveModelVariant.userType() == qMetaTypeId<QJSValue>())
2373 effectiveModelVariant = effectiveModelVariant.value<QJSValue>().toVariant();
2374
2375 const auto instanceModel = qobject_cast<QQmlInstanceModel *>(object: qvariant_cast<QObject*>(v: effectiveModelVariant));
2376
2377 if (instanceModel) {
2378 if (tableModel) {
2379 delete tableModel;
2380 tableModel = nullptr;
2381 }
2382 model = instanceModel;
2383 } else {
2384 if (!tableModel)
2385 createWrapperModel();
2386 tableModel->setModel(effectiveModelVariant);
2387 }
2388
2389 connectToModel();
2390}
2391
2392void QQuickTableViewPrivate::syncSyncView()
2393{
2394 Q_Q(QQuickTableView);
2395
2396 if (assignedSyncView != syncView) {
2397 if (syncView)
2398 syncView->d_func()->syncChildren.removeOne(t: q);
2399
2400 if (assignedSyncView) {
2401 QQuickTableView *view = assignedSyncView;
2402
2403 while (view) {
2404 if (view == q) {
2405 if (!layoutWarningIssued) {
2406 layoutWarningIssued = true;
2407 qmlWarning(me: q) << "TableView: recursive syncView connection detected!";
2408 }
2409 syncView = nullptr;
2410 return;
2411 }
2412 view = view->d_func()->syncView;
2413 }
2414
2415 assignedSyncView->d_func()->syncChildren.append(t: q);
2416 scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2417 }
2418
2419 syncView = assignedSyncView;
2420 }
2421
2422 syncHorizontally = syncView && assignedSyncDirection & Qt::Horizontal;
2423 syncVertically = syncView && assignedSyncDirection & Qt::Vertical;
2424
2425 if (syncHorizontally) {
2426 q->setColumnSpacing(syncView->columnSpacing());
2427 updateContentWidth();
2428 }
2429
2430 if (syncVertically) {
2431 q->setRowSpacing(syncView->rowSpacing());
2432 updateContentHeight();
2433 }
2434
2435 if (syncView && loadedItems.isEmpty() && !tableSize.isEmpty()) {
2436 // When we have a syncView, we can sometimes temporarily end up with no loaded items.
2437 // This can happen if the syncView has a model with more rows or columns than us, in
2438 // which case the viewport can end up in a place where we have no rows or columns to
2439 // show. In that case, check now if the viewport has been flicked back again, and
2440 // that we can rebuild the table with a visible top-left cell.
2441 const auto syncView_d = syncView->d_func();
2442 if (!syncView_d->loadedItems.isEmpty()) {
2443 if (syncHorizontally && syncView_d->leftColumn() <= tableSize.width() - 1)
2444 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::ViewportOnly;
2445 else if (syncVertically && syncView_d->topRow() <= tableSize.height() - 1)
2446 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::ViewportOnly;
2447 }
2448 }
2449}
2450
2451void QQuickTableViewPrivate::connectToModel()
2452{
2453 Q_Q(QQuickTableView);
2454 Q_TABLEVIEW_ASSERT(model, "");
2455
2456 QObjectPrivate::connect(sender: model, signal: &QQmlInstanceModel::createdItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemCreatedCallback);
2457 QObjectPrivate::connect(sender: model, signal: &QQmlInstanceModel::initItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::initItemCallback);
2458 QObjectPrivate::connect(sender: model, signal: &QQmlTableInstanceModel::itemPooled, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemPooledCallback);
2459 QObjectPrivate::connect(sender: model, signal: &QQmlTableInstanceModel::itemReused, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemReusedCallback);
2460
2461 // Connect atYEndChanged to a function that fetches data if more is available
2462 QObjectPrivate::connect(sender: q, signal: &QQuickTableView::atYEndChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::fetchMoreData);
2463
2464 if (auto const aim = model->abstractItemModel()) {
2465 // When the model exposes a QAIM, we connect to it directly. This means that if the current model is
2466 // a QQmlDelegateModel, we just ignore all the change sets it emits. In most cases, the model will instead
2467 // be our own QQmlTableInstanceModel, which doesn't bother creating change sets at all. For models that are
2468 // not based on QAIM (like QQmlObjectModel, QQmlListModel, javascript arrays etc), there is currently no way
2469 // to modify the model at runtime without also re-setting the model on the view.
2470 connect(sender: aim, signal: &QAbstractItemModel::rowsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsMovedCallback);
2471 connect(sender: aim, signal: &QAbstractItemModel::columnsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsMovedCallback);
2472 connect(sender: aim, signal: &QAbstractItemModel::rowsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsInsertedCallback);
2473 connect(sender: aim, signal: &QAbstractItemModel::rowsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsRemovedCallback);
2474 connect(sender: aim, signal: &QAbstractItemModel::columnsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsInsertedCallback);
2475 connect(sender: aim, signal: &QAbstractItemModel::columnsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsRemovedCallback);
2476 connect(sender: aim, signal: &QAbstractItemModel::modelReset, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelResetCallback);
2477 connect(sender: aim, signal: &QAbstractItemModel::layoutChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::layoutChangedCallback);
2478 } else {
2479 QObjectPrivate::connect(sender: model, signal: &QQmlInstanceModel::modelUpdated, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelUpdated);
2480 }
2481}
2482
2483void QQuickTableViewPrivate::disconnectFromModel()
2484{
2485 Q_Q(QQuickTableView);
2486 Q_TABLEVIEW_ASSERT(model, "");
2487
2488 QObjectPrivate::disconnect(sender: model, signal: &QQmlInstanceModel::createdItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemCreatedCallback);
2489 QObjectPrivate::disconnect(sender: model, signal: &QQmlInstanceModel::initItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::initItemCallback);
2490 QObjectPrivate::disconnect(sender: model, signal: &QQmlTableInstanceModel::itemPooled, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemPooledCallback);
2491 QObjectPrivate::disconnect(sender: model, signal: &QQmlTableInstanceModel::itemReused, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemReusedCallback);
2492
2493 QObjectPrivate::disconnect(sender: q, signal: &QQuickTableView::atYEndChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::fetchMoreData);
2494
2495 if (auto const aim = model->abstractItemModel()) {
2496 disconnect(sender: aim, signal: &QAbstractItemModel::rowsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsMovedCallback);
2497 disconnect(sender: aim, signal: &QAbstractItemModel::columnsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsMovedCallback);
2498 disconnect(sender: aim, signal: &QAbstractItemModel::rowsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsInsertedCallback);
2499 disconnect(sender: aim, signal: &QAbstractItemModel::rowsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsRemovedCallback);
2500 disconnect(sender: aim, signal: &QAbstractItemModel::columnsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsInsertedCallback);
2501 disconnect(sender: aim, signal: &QAbstractItemModel::columnsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsRemovedCallback);
2502 disconnect(sender: aim, signal: &QAbstractItemModel::modelReset, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelResetCallback);
2503 disconnect(sender: aim, signal: &QAbstractItemModel::layoutChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::layoutChangedCallback);
2504 } else {
2505 QObjectPrivate::disconnect(sender: model, signal: &QQmlInstanceModel::modelUpdated, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelUpdated);
2506 }
2507}
2508
2509void QQuickTableViewPrivate::modelUpdated(const QQmlChangeSet &changeSet, bool reset)
2510{
2511 Q_UNUSED(changeSet);
2512 Q_UNUSED(reset);
2513
2514 Q_TABLEVIEW_ASSERT(!model->abstractItemModel(), "");
2515 scheduleRebuildTable(options: RebuildOption::ViewportOnly
2516 | RebuildOption::CalculateNewContentWidth
2517 | RebuildOption::CalculateNewContentHeight);
2518}
2519
2520void QQuickTableViewPrivate::rowsMovedCallback(const QModelIndex &parent, int, int, const QModelIndex &, int )
2521{
2522 if (parent != QModelIndex())
2523 return;
2524
2525 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
2526}
2527
2528void QQuickTableViewPrivate::columnsMovedCallback(const QModelIndex &parent, int, int, const QModelIndex &, int)
2529{
2530 if (parent != QModelIndex())
2531 return;
2532
2533 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
2534}
2535
2536void QQuickTableViewPrivate::rowsInsertedCallback(const QModelIndex &parent, int, int)
2537{
2538 if (parent != QModelIndex())
2539 return;
2540
2541 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
2542}
2543
2544void QQuickTableViewPrivate::rowsRemovedCallback(const QModelIndex &parent, int, int)
2545{
2546 if (parent != QModelIndex())
2547 return;
2548
2549 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
2550}
2551
2552void QQuickTableViewPrivate::columnsInsertedCallback(const QModelIndex &parent, int, int)
2553{
2554 if (parent != QModelIndex())
2555 return;
2556
2557 // Adding a column (or row) can result in the table going from being
2558 // e.g completely inside the viewport to go outside. And in the latter
2559 // case, the user needs to be able to scroll the viewport, also if
2560 // flags such as Flickable.StopAtBounds is in use. So we need to
2561 // update contentWidth to support that case.
2562 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
2563}
2564
2565void QQuickTableViewPrivate::columnsRemovedCallback(const QModelIndex &parent, int, int)
2566{
2567 if (parent != QModelIndex())
2568 return;
2569
2570 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
2571}
2572
2573void QQuickTableViewPrivate::layoutChangedCallback(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint)
2574{
2575 Q_UNUSED(parents);
2576 Q_UNUSED(hint);
2577
2578 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
2579}
2580
2581void QQuickTableViewPrivate::fetchMoreData()
2582{
2583 if (tableModel && tableModel->canFetchMore()) {
2584 tableModel->fetchMore();
2585 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
2586 }
2587}
2588
2589void QQuickTableViewPrivate::modelResetCallback()
2590{
2591 scheduleRebuildTable(options: RebuildOption::All);
2592}
2593
2594void QQuickTableViewPrivate::scheduleRebuildIfFastFlick()
2595{
2596 Q_Q(QQuickTableView);
2597 // If the viewport has moved more than one page vertically or horizontally, we switch
2598 // strategy from refilling edges around the current table to instead rebuild the table
2599 // from scratch inside the new viewport. This will greatly improve performance when flicking
2600 // a long distance in one go, which can easily happen when dragging on scrollbars.
2601 // Note that we don't want to update the content size in this case, since first of all, the
2602 // content size should logically not change as a result of flicking. But more importantly, updating
2603 // the content size in combination with fast-flicking has a tendency to cause flicker in the viewport.
2604
2605 // Check the viewport moved more than one page vertically
2606 if (!viewportRect.intersects(r: QRectF(viewportRect.x(), q->contentY(), 1, q->height()))) {
2607 scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow;
2608 scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2609 }
2610
2611 // Check the viewport moved more than one page horizontally
2612 if (!viewportRect.intersects(r: QRectF(q->contentX(), viewportRect.y(), q->width(), 1))) {
2613 scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn;
2614 scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2615 }
2616}
2617
2618void QQuickTableViewPrivate::setLocalViewportX(qreal contentX)
2619{
2620 // Set the new viewport position if changed, but don't trigger any
2621 // rebuilds or updates. We use this function internally to distinguish
2622 // external flicking from internal sync-ing of the content view.
2623 Q_Q(QQuickTableView);
2624 QBoolBlocker blocker(inSetLocalViewportPos, true);
2625
2626 if (qFuzzyCompare(p1: contentX, p2: q->contentX()))
2627 return;
2628
2629 q->setContentX(contentX);
2630}
2631
2632void QQuickTableViewPrivate::setLocalViewportY(qreal contentY)
2633{
2634 // Set the new viewport position if changed, but don't trigger any
2635 // rebuilds or updates. We use this function internally to distinguish
2636 // external flicking from internal sync-ing of the content view.
2637 Q_Q(QQuickTableView);
2638 QBoolBlocker blocker(inSetLocalViewportPos, true);
2639
2640 if (qFuzzyCompare(p1: contentY, p2: q->contentY()))
2641 return;
2642
2643 q->setContentY(contentY);
2644}
2645
2646void QQuickTableViewPrivate::syncViewportRect()
2647{
2648 // Sync viewportRect so that it contains the actual geometry of the viewport
2649 Q_Q(QQuickTableView);
2650 viewportRect = QRectF(q->contentX(), q->contentY(), q->width(), q->height());
2651 qCDebug(lcTableViewDelegateLifecycle) << viewportRect;
2652}
2653
2654void QQuickTableViewPrivate::syncViewportPosRecursive()
2655{
2656 Q_Q(QQuickTableView);
2657 QBoolBlocker recursionGuard(inSyncViewportPosRecursive, true);
2658
2659 if (syncView) {
2660 auto syncView_d = syncView->d_func();
2661 if (!syncView_d->inSyncViewportPosRecursive) {
2662 if (syncHorizontally)
2663 syncView_d->setLocalViewportX(q->contentX());
2664 if (syncVertically)
2665 syncView_d->setLocalViewportY(q->contentY());
2666 syncView_d->syncViewportPosRecursive();
2667 }
2668 }
2669
2670 for (auto syncChild : qAsConst(t&: syncChildren)) {
2671 auto syncChild_d = syncChild->d_func();
2672 if (!syncChild_d->inSyncViewportPosRecursive) {
2673 if (syncChild_d->syncHorizontally)
2674 syncChild_d->setLocalViewportX(q->contentX());
2675 if (syncChild_d->syncVertically)
2676 syncChild_d->setLocalViewportY(q->contentY());
2677 syncChild_d->syncViewportPosRecursive();
2678 }
2679 }
2680}
2681
2682QQuickTableView::QQuickTableView(QQuickItem *parent)
2683 : QQuickFlickable(*(new QQuickTableViewPrivate), parent)
2684{
2685 setFlag(flag: QQuickItem::ItemIsFocusScope);
2686}
2687
2688QQuickTableView::~QQuickTableView()
2689{
2690}
2691
2692QQuickTableView::QQuickTableView(QQuickTableViewPrivate &dd, QQuickItem *parent)
2693 : QQuickFlickable(dd, parent)
2694{
2695 setFlag(flag: QQuickItem::ItemIsFocusScope);
2696}
2697
2698qreal QQuickTableView::minXExtent() const
2699{
2700 return QQuickFlickable::minXExtent() - d_func()->origin.x();
2701}
2702
2703qreal QQuickTableView::maxXExtent() const
2704{
2705 return QQuickFlickable::maxXExtent() - d_func()->endExtent.width();
2706}
2707
2708qreal QQuickTableView::minYExtent() const
2709{
2710 return QQuickFlickable::minYExtent() - d_func()->origin.y();
2711}
2712
2713qreal QQuickTableView::maxYExtent() const
2714{
2715 return QQuickFlickable::maxYExtent() - d_func()->endExtent.height();
2716}
2717
2718int QQuickTableView::rows() const
2719{
2720 return d_func()->tableSize.height();
2721}
2722
2723int QQuickTableView::columns() const
2724{
2725 return d_func()->tableSize.width();
2726}
2727
2728qreal QQuickTableView::rowSpacing() const
2729{
2730 return d_func()->cellSpacing.height();
2731}
2732
2733void QQuickTableView::setRowSpacing(qreal spacing)
2734{
2735 Q_D(QQuickTableView);
2736 if (qt_is_nan(d: spacing) || !qt_is_finite(d: spacing))
2737 return;
2738 if (qFuzzyCompare(p1: d->cellSpacing.height(), p2: spacing))
2739 return;
2740
2741 d->cellSpacing.setHeight(spacing);
2742 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::LayoutOnly
2743 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentHeight);
2744 emit rowSpacingChanged();
2745}
2746
2747qreal QQuickTableView::columnSpacing() const
2748{
2749 return d_func()->cellSpacing.width();
2750}
2751
2752void QQuickTableView::setColumnSpacing(qreal spacing)
2753{
2754 Q_D(QQuickTableView);
2755 if (qt_is_nan(d: spacing) || !qt_is_finite(d: spacing))
2756 return;
2757 if (qFuzzyCompare(p1: d->cellSpacing.width(), p2: spacing))
2758 return;
2759
2760 d->cellSpacing.setWidth(spacing);
2761 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::LayoutOnly
2762 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentWidth);
2763 emit columnSpacingChanged();
2764}
2765
2766QJSValue QQuickTableView::rowHeightProvider() const
2767{
2768 return d_func()->rowHeightProvider;
2769}
2770
2771void QQuickTableView::setRowHeightProvider(const QJSValue &provider)
2772{
2773 Q_D(QQuickTableView);
2774 if (provider.strictlyEquals(other: d->rowHeightProvider))
2775 return;
2776
2777 d->rowHeightProvider = provider;
2778 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly
2779 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentHeight);
2780 emit rowHeightProviderChanged();
2781}
2782
2783QJSValue QQuickTableView::columnWidthProvider() const
2784{
2785 return d_func()->columnWidthProvider;
2786}
2787
2788void QQuickTableView::setColumnWidthProvider(const QJSValue &provider)
2789{
2790 Q_D(QQuickTableView);
2791 if (provider.strictlyEquals(other: d->columnWidthProvider))
2792 return;
2793
2794 d->columnWidthProvider = provider;
2795 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly
2796 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentWidth);
2797 emit columnWidthProviderChanged();
2798}
2799
2800QVariant QQuickTableView::model() const
2801{
2802 return d_func()->modelImpl();
2803}
2804
2805void QQuickTableView::setModel(const QVariant &newModel)
2806{
2807 return d_func()->setModelImpl(newModel);
2808}
2809
2810QQmlComponent *QQuickTableView::delegate() const
2811{
2812 return d_func()->assignedDelegate;
2813}
2814
2815void QQuickTableView::setDelegate(QQmlComponent *newDelegate)
2816{
2817 Q_D(QQuickTableView);
2818 if (newDelegate == d->assignedDelegate)
2819 return;
2820
2821 d->assignedDelegate = newDelegate;
2822 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::All);
2823
2824 emit delegateChanged();
2825}
2826
2827bool QQuickTableView::reuseItems() const
2828{
2829 return bool(d_func()->reusableFlag == QQmlTableInstanceModel::Reusable);
2830}
2831
2832void QQuickTableView::setReuseItems(bool reuse)
2833{
2834 Q_D(QQuickTableView);
2835 if (reuseItems() == reuse)
2836 return;
2837
2838 d->reusableFlag = reuse ? QQmlTableInstanceModel::Reusable : QQmlTableInstanceModel::NotReusable;
2839
2840 if (!reuse && d->tableModel) {
2841 // When we're told to not reuse items, we
2842 // immediately, as documented, drain the pool.
2843 d->tableModel->drainReusableItemsPool(maxPoolTime: 0);
2844 }
2845
2846 emit reuseItemsChanged();
2847}
2848
2849void QQuickTableView::setContentWidth(qreal width)
2850{
2851 Q_D(QQuickTableView);
2852 d->explicitContentWidth = width;
2853 QQuickFlickable::setContentWidth(width);
2854}
2855
2856void QQuickTableView::setContentHeight(qreal height)
2857{
2858 Q_D(QQuickTableView);
2859 d->explicitContentHeight = height;
2860 QQuickFlickable::setContentHeight(height);
2861}
2862
2863/*!
2864 \qmlproperty TableView QtQuick::TableView::syncView
2865
2866 If this property of a TableView is set to another TableView, both the
2867 tables will synchronize with regard to flicking, column widths/row heights,
2868 and spacing according to \l syncDirection.
2869
2870 If \l syncDirection contains \l Qt.Horizontal, current tableView's column
2871 widths, column spacing, and horizontal flicking movement synchronizes with
2872 syncView's.
2873
2874 If \l syncDirection contains \l Qt.Vertical, current tableView's row
2875 heights, row spacing, and vertical flicking movement synchronizes with
2876 syncView's.
2877
2878 \sa syncDirection
2879*/
2880QQuickTableView *QQuickTableView::syncView() const
2881{
2882 return d_func()->assignedSyncView;
2883}
2884
2885void QQuickTableView::setSyncView(QQuickTableView *view)
2886{
2887 Q_D(QQuickTableView);
2888 if (d->assignedSyncView == view)
2889 return;
2890
2891 d->assignedSyncView = view;
2892 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly);
2893
2894 emit syncViewChanged();
2895}
2896
2897/*!
2898 \qmlproperty Qt::Orientations QtQuick::TableView::syncDirection
2899
2900 If the \l syncView is set on a TableView, this property controls
2901 synchronization of flicking direction(s) for both tables. The default is \c
2902 {Qt.Horizontal | Qt.Vertical}, which means that if you flick either table
2903 in either direction, the other table is flicked the same amount in the
2904 same direction.
2905
2906 This property and \l syncView can be used to make two tableViews
2907 synchronize with each other smoothly in flicking regardless of the different
2908 overshoot/undershoot, velocity, acceleration/deceleration or rebound
2909 animation, and so on.
2910
2911 A typical use case is to make several headers flick along with the table.
2912
2913 \sa syncView, headerView
2914*/
2915Qt::Orientations QQuickTableView::syncDirection() const
2916{
2917 return d_func()->assignedSyncDirection;
2918}
2919
2920void QQuickTableView::setSyncDirection(Qt::Orientations direction)
2921{
2922 Q_D(QQuickTableView);
2923 if (d->assignedSyncDirection == direction)
2924 return;
2925
2926 d->assignedSyncDirection = direction;
2927 if (d->assignedSyncView)
2928 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly);
2929
2930 emit syncDirectionChanged();
2931}
2932
2933void QQuickTableView::forceLayout()
2934{
2935 d_func()->forceLayout();
2936}
2937
2938QQuickTableViewAttached *QQuickTableView::qmlAttachedProperties(QObject *obj)
2939{
2940 return new QQuickTableViewAttached(obj);
2941}
2942
2943void QQuickTableView::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
2944{
2945 Q_D(QQuickTableView);
2946 QQuickFlickable::geometryChanged(newGeometry, oldGeometry);
2947
2948 if (d->tableModel) {
2949 // When the view changes size, we force the pool to
2950 // shrink by releasing all pooled items.
2951 d->tableModel->drainReusableItemsPool(maxPoolTime: 0);
2952 }
2953
2954 polish();
2955}
2956
2957void QQuickTableView::viewportMoved(Qt::Orientations orientation)
2958{
2959 Q_D(QQuickTableView);
2960
2961 // If the new viewport position was set from the setLocalViewportXY()
2962 // functions, we just update the position silently and return. Otherwise, if
2963 // the viewport was flicked by the user, or some other control, we
2964 // recursively sync all the views in the hierarchy to the same position.
2965 QQuickFlickable::viewportMoved(orient: orientation);
2966 if (d->inSetLocalViewportPos)
2967 return;
2968
2969 // Move all views in the syncView hierarchy to the same contentX/Y.
2970 // We need to start from this view (and not the root syncView) to
2971 // ensure that we respect all the individual syncDirection flags
2972 // between the individual views in the hierarchy.
2973 d->syncViewportPosRecursive();
2974
2975 auto rootView = d->rootSyncView();
2976 auto rootView_d = rootView->d_func();
2977
2978 rootView_d->scheduleRebuildIfFastFlick();
2979
2980 if (!rootView_d->polishScheduled) {
2981 if (rootView_d->scheduledRebuildOptions) {
2982 // When we need to rebuild, collecting several viewport
2983 // moves and do a single polish gives a quicker UI.
2984 rootView->polish();
2985 } else {
2986 // Updating the table right away when flicking
2987 // slowly gives a smoother experience.
2988 const bool updated = rootView->d_func()->updateTableRecursive();
2989 if (!updated) {
2990 // One, or more, of the views are already in an
2991 // update, so we need to wait a cycle.
2992 rootView->polish();
2993 }
2994 }
2995 }
2996}
2997
2998void QQuickTableViewPrivate::_q_componentFinalized()
2999{
3000 // Now that all bindings are evaluated, and we know
3001 // our final geometery, we can build the table.
3002 qCDebug(lcTableViewDelegateLifecycle);
3003 updatePolish();
3004}
3005
3006void QQuickTableViewPrivate::registerCallbackWhenBindingsAreEvaluated()
3007{
3008 // componentComplete() is called on us after all static values have been assigned, but
3009 // before bindings to any anchestors has been evaluated. Especially this means that
3010 // if our size is bound to the parents size, it will still be empty at that point.
3011 // And we cannot build the table without knowing our own size. We could wait until we
3012 // got the first updatePolish() callback, but at that time, any asynchronous loaders that we
3013 // might be inside have already finished loading, which means that we would load all
3014 // the delegate items synchronously instead of asynchronously. We therefore add a componentFinalized
3015 // function that gets called after all the bindings we rely on has been evaluated.
3016 // When receiving this call, we load the delegate items (and build the table).
3017 Q_Q(QQuickTableView);
3018 QQmlEnginePrivate *engPriv = QQmlEnginePrivate::get(e: qmlEngine(q));
3019 static int finalizedIdx = -1;
3020 if (finalizedIdx < 0)
3021 finalizedIdx = q->metaObject()->indexOfSlot(slot: "_q_componentFinalized()");
3022 engPriv->registerFinalizeCallback(obj: q, index: finalizedIdx);
3023}
3024
3025void QQuickTableView::componentComplete()
3026{
3027 QQuickFlickable::componentComplete();
3028 d_func()->registerCallbackWhenBindingsAreEvaluated();
3029}
3030
3031class QObjectPrivate;
3032class QQuickTableSectionSizeProviderPrivate : public QObjectPrivate {
3033public:
3034 QQuickTableSectionSizeProviderPrivate();
3035 ~QQuickTableSectionSizeProviderPrivate();
3036 QHash<int, qreal> hash;
3037};
3038
3039QQuickTableSectionSizeProvider::QQuickTableSectionSizeProvider(QObject *parent)
3040 : QObject (*(new QQuickTableSectionSizeProviderPrivate), parent)
3041{
3042}
3043
3044void QQuickTableSectionSizeProvider::setSize(int section, qreal size)
3045{
3046 Q_D(QQuickTableSectionSizeProvider);
3047 if (section < 0 || size < 0) {
3048 qmlWarning(me: this) << "setSize: section or size less than zero";
3049 return;
3050 }
3051 if (qFuzzyCompare(p1: QQuickTableSectionSizeProvider::size(section), p2: size))
3052 return;
3053 d->hash.insert(key: section, value: size);
3054 emit sizeChanged();
3055}
3056
3057// return -1.0 if no valid explicit size retrieved
3058qreal QQuickTableSectionSizeProvider::size(int section)
3059{
3060 Q_D(QQuickTableSectionSizeProvider);
3061 auto it = d->hash.find(key: section);
3062 if (it != d->hash.end())
3063 return *it;
3064 return -1.0;
3065}
3066
3067// return true if section is valid
3068bool QQuickTableSectionSizeProvider::resetSize(int section)
3069{
3070 Q_D(QQuickTableSectionSizeProvider);
3071 if (d->hash.empty())
3072 return false;
3073
3074 auto ret = d->hash.remove(key: section);
3075 if (ret)
3076 emit sizeChanged();
3077 return ret;
3078}
3079
3080void QQuickTableSectionSizeProvider::resetAll()
3081{
3082 Q_D(QQuickTableSectionSizeProvider);
3083 d->hash.clear();
3084 emit sizeChanged();
3085}
3086
3087QQuickTableSectionSizeProviderPrivate::QQuickTableSectionSizeProviderPrivate()
3088 : QObjectPrivate()
3089{
3090}
3091
3092QQuickTableSectionSizeProviderPrivate::~QQuickTableSectionSizeProviderPrivate()
3093{
3094
3095}
3096#include "moc_qquicktableview_p.cpp"
3097
3098QT_END_NAMESPACE
3099

source code of qtdeclarative/src/quick/items/qquicktableview.cpp