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