1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquicktableview_p.h"
5#include "qquicktableview_p_p.h"
6
7#include <QtCore/qtimer.h>
8#include <QtCore/qdir.h>
9#include <QtQmlModels/private/qqmldelegatemodel_p.h>
10#include <QtQmlModels/private/qqmldelegatemodel_p_p.h>
11#include <QtQml/private/qqmlincubator_p.h>
12#include <QtQmlModels/private/qqmlchangeset_p.h>
13#include <QtQml/qqmlinfo.h>
14
15#include <QtQuick/private/qquickflickable_p_p.h>
16#include <QtQuick/private/qquickitemviewfxitem_p_p.h>
17#include <QtQuick/private/qquicktaphandler_p.h>
18
19/*!
20 \qmltype TableView
21 \inqmlmodule QtQuick
22 \since 5.12
23 \ingroup qtquick-views
24 \inherits Flickable
25 \brief Provides a table view of items to display data from a model.
26
27 A TableView has a \l model that defines the data to be displayed, and a
28 \l delegate that defines how the data should be displayed.
29
30 TableView inherits \l Flickable. This means that while the model can have
31 any number of rows and columns, only a subsection of the table is usually
32 visible inside the viewport. As soon as you flick, new rows and columns
33 enter the viewport, while old ones exit and are removed from the viewport.
34 The rows and columns that move out are reused for building the rows and columns
35 that move into the viewport. As such, the TableView support models of any
36 size without affecting performance.
37
38 A TableView displays data from models created from built-in QML types
39 such as ListModel and XmlListModel, which populates the first column only
40 in a TableView. To create models with multiple columns, either use
41 \l TableModel or a C++ model that inherits QAbstractItemModel.
42
43 A TableView does not include headers by default. You can add headers
44 using the \l HorizontalHeaderView and \l VerticalHeaderView from
45 Qt Quick Controls.
46
47 \note TableView will only \l {isRowLoaded()}{load} as many delegate items as
48 needed to fill up the view. There is no guarantee that items outside the view
49 will be loaded, although TableView will sometimes pre-load items for
50 optimization reasons. Hence, a TableView with zero width or height might not
51 load any delegate items at all.
52
53 \section1 Example Usage
54
55 \section2 C++ Models
56
57 The following example shows how to create a model from C++ with multiple
58 columns:
59
60 \snippet qml/tableview/cpp-tablemodel.h 0
61
62 And then how to use it from QML:
63
64 \snippet qml/tableview/cpp-tablemodel.qml 0
65
66 \section2 QML Models
67
68 For prototyping and displaying very simple data (from a web API, for
69 example), \l TableModel can be used:
70
71 \snippet qml/tableview/qml-tablemodel.qml 0
72
73 \section1 Reusing items
74
75 TableView recycles delegate items by default, instead of instantiating from
76 the \l delegate whenever new rows and columns are flicked into view. This
77 approach gives a huge performance boost, depending on the complexity of the
78 delegate.
79
80 When an item is flicked out, it moves to the \e{reuse pool}, which is an
81 internal cache of unused items. When this happens, the \l TableView::pooled
82 signal is emitted to inform the item about it. Likewise, when the item is
83 moved back from the pool, the \l TableView::reused signal is emitted.
84
85 Any item properties that come from the model are updated when the
86 item is reused. This includes \c index, \c row, and \c column, but also
87 any model roles.
88
89 \note Avoid storing any state inside a delegate. If you do, reset it
90 manually on receiving the \l TableView::reused signal.
91
92 If an item has timers or animations, consider pausing them on receiving
93 the \l TableView::pooled signal. That way you avoid using the CPU resources
94 for items that are not visible. Likewise, if an item has resources that
95 cannot be reused, they could be freed up.
96
97 If you don't want to reuse items or if the \l delegate cannot support it,
98 you can set the \l reuseItems property to \c false.
99
100 \note While an item is in the pool, it might still be alive and respond
101 to connected signals and bindings.
102
103 The following example shows a delegate that animates a spinning rectangle. When
104 it is pooled, the animation is temporarily paused:
105
106 \snippet qml/tableview/reusabledelegate.qml 0
107
108 \section1 Row heights and column widths
109
110 When a new column is flicked into view, TableView will determine its width
111 by calling the \l columnWidthProvider. If set, this function will alone decide
112 the width of the column. Otherwise, it will check if an explicit width has
113 been set with \l setColumnWidth(). If not, \l implicitColumnWidth() will be used.
114 The implicit width of a column is the same as the largest
115 \l {Item::implicitWidth}{implicit width} found among the currently loaded
116 delegate items in that column. Trying to set an explicit \c width directly on
117 a delegate has no effect, and will be ignored and overwritten. The same logic also
118 applies to row heights.
119
120 An implementation of a columnWidthProvider that is equivalent to the default
121 logic would be:
122
123 \code
124 columnWidthProvider: function(column) {
125 let w = explicitColumnWidth(column)
126 if (w >= 0)
127 return w;
128 return implicitColumnWidth(column)
129 }
130 \endcode
131
132 Once the column width is resolved, all other items in the same column are resized
133 to this width, including any items that are flicked into the view at a later point.
134
135 \note The resolved width of a column is discarded when the whole column is flicked out
136 of the view, and is recalculated again if it's flicked back in. This means that if the
137 width depends on the \l implicitColumnWidth(), the calculation can be different each time,
138 depending on which row you're at when the column enters (since \l implicitColumnWidth()
139 only considers the delegate items that are currently \l {isColumnLoaded()}{loaded}).
140 To avoid this, you should use a \l columnWidthProvider, or ensure that all the delegate
141 items in the same column have the same \c implicitWidth.
142
143 If you change the values that a \l rowHeightProvider or a
144 \l columnWidthProvider return for rows and columns inside the viewport, you
145 must call \l forceLayout. This informs TableView that it needs to use the
146 provider functions again to recalculate and update the layout.
147
148 Since Qt 5.13, if you want to hide a specific column, you can return \c 0
149 from the \l columnWidthProvider for that column. Likewise, you can return 0
150 from the \l rowHeightProvider to hide a row. If you return a negative
151 number, TableView will fall back to calculate the size based on the delegate
152 items.
153
154 \note The size of a row or column should be a whole number to avoid
155 sub-pixel alignment of items.
156
157 The following example shows how to set a simple \c columnWidthProvider
158 together with a timer that modifies the values the function returns. When
159 the array is modified, \l forceLayout is called to let the changes
160 take effect:
161
162 \snippet qml/tableview/tableviewwithprovider.qml 0
163
164 \section1 Editing cells
165
166 You can let the user edit table cells by providing an edit delegate. The
167 edit delegate will be instantiated according to the \l editTriggers, which
168 by default is when the user double taps on a cell, or presses e.g
169 \l Qt::Key_Enter or \l Qt::Key_Return. The edit delegate is set using
170 \l {TableView::editDelegate}, which is an attached property that you set
171 on the \l delegate. The following snippet shows how to do that:
172
173 \snippet qml/tableview/editdelegate.qml 0
174
175 If the user presses Qt::Key_Enter or Qt::Key_Return while the edit delegate
176 is active, TableView will emit the \l TableView::commit signal to the edit
177 delegate, so that it can write back the changed data to the model.
178
179 \note In order for a cell to be editable, the model needs to override
180 \l QAbstractItemModel::flags(), and return \c Qt::ItemIsEditable.
181 This flag is not enabled in QAbstractItemModel by default.
182 The override could for example look like this:
183
184 \code
185 Qt::ItemFlags QAbstractItemModelSubClass::flags(const QModelIndex &index) const override
186 {
187 Q_UNUSED(index)
188 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
189 }
190 \endcode
191
192 If the \l {TableView::delegate}{TableView delegate} has a property
193 \c {required property bool editing} defined, it will be set to \c true
194 for the delegate being edited. See the documentation for
195 \l editDelegate for an example on how to use it.
196
197 \sa TableView::editDelegate, TableView::commit, editTriggers, edit(), closeEditor()
198
199 \section1 Overlays and underlays
200
201 All new items that are instantiated from the delegate are parented to the
202 \l{Flickable::}{contentItem} with the \c z value, \c 1. You can add your
203 own items inside the Tableview, as child items of the Flickable. By
204 controlling their \c z value, you can make them be on top of or
205 underneath the table items.
206
207 Here is an example that shows how to add some text on top of the table, that
208 moves together with the table as you flick:
209
210 \snippet qml/tableview/tableviewwithheader.qml 0
211
212 Here is another example that shows how to create an overlay item that
213 stays on top of a particular cell. This requires a bit more code, since
214 the location of a cell will \l {layoutChanged}{change} if the user, for
215 example, is resizing a column in front of it.
216
217 \snippet qml/tableview/overlay.qml 0
218
219 You could also parent the overlay directly to the cell instead of the
220 \l {Flickable::}{contentItem}. But doing so will be fragile since the cell is unloaded
221 or reused whenever it's flicked out of the viewport.
222
223 \sa layoutChanged()
224
225 \section1 Selecting items
226
227 You can add selection support to TableView by assigning an \l ItemSelectionModel to
228 the \l selectionModel property. It will then use this model to control which
229 delegate items should be shown as selected, and which item should be shown as
230 current. You can set \l selectionBehavior to control if the user should
231 be allowed to select individual cells, rows, or columns.
232
233 To find out whether a delegate is selected or current, declare the
234 following properties:
235
236 \code
237 delegate: Item {
238 required property bool selected
239 required property bool current
240 // ...
241 }
242 \endcode
243
244 \note the \c selected and \c current properties must be defined as \c required.
245 This will inform TableView that it should take responsibility for updating their
246 values. If not, they will simply be ignored. See also \l {Required Properties}.
247
248 The following snippet shows how an application can render the delegate differently
249 depending on the \c selected property:
250
251 \snippet qml/tableview/selectionmodel.qml 0
252
253 The \l currentRow and \l currentColumn properties can also be useful if you need
254 to render a delegate differently depending on if it lies on the same row or column
255 as the current item.
256
257 \note \l{Qt Quick Controls} offers a SelectionRectangle that can be used
258 to let the user select cells.
259
260 \note By default, a cell will become
261 \l {ItemSelectionModel::currentIndex}{current}, and any selections will
262 be removed, when the user taps on it. If such default tap behavior is not wanted
263 (e.g if you use custom pointer handlers inside your delegate), you can set
264 \l pointerNavigationEnabled to \c false.
265
266 \section1 Keyboard navigation
267
268 In order to support keyboard navigation, you need to assign an \l ItemSelectionModel
269 to the \l selectionModel property. TableView will then use this model to manipulate
270 the model's \l {ItemSelectionModel::currentIndex}{currentIndex}.
271
272 It's the responsibility of the delegate to render itself as
273 \l {ItemSelectionModel::currentIndex}{current}. You can do this by adding a
274 property \c {required property bool current} to it, and let the appearance
275 depend on its state. The \c current property's value is set by the TableView.
276 You can also disable keyboard navigation fully (in case you want to implement your
277 own key handlers) by setting \l keyNavigationEnabled to \c false.
278
279 The following example demonstrates how you can use keyboard navigation together
280 with \c current and \c selected properties:
281
282 \snippet qml/tableview/keyboard-navigation.qml 0
283
284 \section1 Copy and paste
285
286 Implementing copy and paste operations for a TableView usually also includes using
287 a QUndoStack (or some other undo/redo framework). The QUndoStack can be used to
288 store the different operations done on the model, like adding or removing rows, or
289 pasting data from the clipboard, with a way to undo it again later. However, an
290 accompanying QUndoStack that describes the possible operations, and how to undo them,
291 should be designed according to the needs of the model and the application.
292 As such, TableView doesn't offer a built-in API for handling copy and paste.
293
294 The following snippet can be used as a reference for how to add copy and paste support
295 to your model and TableView. It uses the existing mime data API in QAbstractItemModel,
296 together with QClipboard. The snippet will work as it is, but can also be extended to
297 use a QUndoStack.
298
299 \code
300 // Inside your C++ QAbstractTableModel subclass:
301
302 Q_INVOKABLE void copyToClipboard(const QModelIndexList &indexes) const
303 {
304 QGuiApplication::clipboard()->setMimeData(mimeData(indexes));
305 }
306
307 Q_INVOKABLE bool pasteFromClipboard(const QModelIndex &targetIndex)
308 {
309 const QMimeData *mimeData = QGuiApplication::clipboard()->mimeData();
310 // Consider using a QUndoCommand for the following call. It should store
311 // the (mime) data for the model items that are about to be overwritten, so
312 // that a later call to undo can revert it.
313 return dropMimeData(mimeData, Qt::CopyAction, -1, -1, targetIndex);
314 }
315 \endcode
316
317 The two functions can, for example, be used from QML like this:
318
319 \code
320 TableView {
321 id: tableView
322 model: tableModel
323 selectionModel: ItemSelectionModel {}
324
325 Shortcut {
326 sequence: StandardKey.Copy
327 onActivated: {
328 let indexes = tableView.selectionModel.selectedIndexes
329 tableView.model.copyToClipboard(indexes)
330 }
331 }
332
333 Shortcut {
334 sequence: StandardKey.Paste
335 onActivated: {
336 let targetIndex = tableView.selectionModel.currentIndex
337 tableView.model.pasteFromClipboard(targetIndex)
338 }
339 }
340 }
341 \endcode
342
343 \sa QAbstractItemModel::mimeData(), QAbstractItemModel::dropMimeData(), QUndoStack, QUndoCommand, QClipboard
344*/
345
346/*!
347 \qmlproperty int QtQuick::TableView::rows
348 \readonly
349
350 This property holds the number of rows in the table.
351
352 \note \a rows is usually equal to the number of rows in the model, but can
353 temporarily differ until all pending model changes have been processed.
354
355 This property is read only.
356*/
357
358/*!
359 \qmlproperty int QtQuick::TableView::columns
360 \readonly
361
362 This property holds the number of columns in the table.
363
364 \note \a columns is usually equal to the number of columns in the model, but
365 can temporarily differ until all pending model changes have been processed.
366
367 If the model is a list, columns will be \c 1.
368
369 This property is read only.
370*/
371
372/*!
373 \qmlproperty real QtQuick::TableView::rowSpacing
374
375 This property holds the spacing between the rows.
376
377 The default value is \c 0.
378*/
379
380/*!
381 \qmlproperty real QtQuick::TableView::columnSpacing
382
383 This property holds the spacing between the columns.
384
385 The default value is \c 0.
386*/
387
388/*!
389 \qmlproperty var QtQuick::TableView::rowHeightProvider
390
391 This property can hold a function that returns the row height for each row
392 in the model. It is called whenever TableView needs to know the height of
393 a specific row. The function takes one argument, \c row, for which the
394 TableView needs to know the height.
395
396 Since Qt 5.13, if you want to hide a specific row, you can return \c 0
397 height for that row. If you return a negative number, TableView calculates
398 the height based on the delegate items.
399
400 \note The rowHeightProvider will usually be called two times when
401 a row is about to load (or when doing layout). First, to know if
402 the row is visible and should be loaded. And second, to determine
403 the height of the row after all items have been loaded.
404 If you need to calculate the row height based on the size of the delegate
405 items, you need to wait for the second call, when all the items have been loaded.
406 You can check for this by calling \l {isRowLoaded()}{isRowLoaded(row)},
407 and simply return -1 if that is not yet the case.
408
409 \sa rowHeightProvider, isRowLoaded(), {Row heights and column widths}
410*/
411
412/*!
413 \qmlproperty var QtQuick::TableView::columnWidthProvider
414
415 This property can hold a function that returns the column width for each
416 column in the model. It is called whenever TableView needs to know the
417 width of a specific column. The function takes one argument, \c column,
418 for which the TableView needs to know the width.
419
420 Since Qt 5.13, if you want to hide a specific column, you can return \c 0
421 width for that column. If you return a negative number, TableView
422 calculates the width based on the delegate items.
423
424 \note The columnWidthProvider will usually be called two times when
425 a column is about to load (or when doing layout). First, to know if
426 the column is visible and should be loaded. And second, to determine
427 the width of the column after all items have been loaded.
428 If you need to calculate the column width based on the size of the delegate
429 items, you need to wait for the second call, when all the items have been loaded.
430 You can check for this by calling \l {isColumnLoaded}{isColumnLoaded(column)},
431 and simply return -1 if that is not yet the case.
432
433 \sa rowHeightProvider, isColumnLoaded(), {Row heights and column widths}
434*/
435
436/*!
437 \qmlproperty model QtQuick::TableView::model
438 This property holds the model that provides data for the table.
439
440 The model provides the set of data that is used to create the items
441 in the view. Models can be created directly in QML using \l TableModel,
442 \l ListModel, \l ObjectModel, or provided by a custom
443 C++ model class. The C++ model must be a subclass of \l QAbstractItemModel
444 or a simple list.
445
446 \sa {qml-data-models}{Data Models}
447*/
448
449/*!
450 \qmlproperty Component QtQuick::TableView::delegate
451
452 The delegate provides a template defining each cell item instantiated by the
453 view. The model index is exposed as an accessible \c index property. The same
454 applies to \c row and \c column. Properties of the model are also available
455 depending upon the type of \l {qml-data-models}{Data Model}.
456
457 A delegate should specify its size using \l{Item::}{implicitWidth} and
458 \l {Item::}{implicitHeight}. The TableView lays out the items based on that
459 information. Explicit width or height settings are ignored and overwritten.
460
461 Inside the delegate, you can optionally add one or more of the following
462 properties. TableView modifies the values of these properties to inform the
463 delegate which state it's in. This can be used by the delegate to render
464 itself differently according on its own state.
465
466 \list
467 \li required property bool current - \c true if the delegate is \l {Keyboard navigation}{current.}
468 \li required property bool selected - \c true if the delegate is \l {Selecting items}{selected.}
469 \li required property bool editing - \c true if the delegate is being \l {Editing cells}{edited.}
470 \endlist
471
472 The following example shows how to use these properties:
473 \code
474 delegate: Rectangle {
475 required property bool current
476 required property bool selected
477 border.width: current ? 1 : 0
478 color: selected ? palette.highlight : palette.base
479 }
480 \endcode
481
482 \note Delegates are instantiated as needed and may be destroyed at any time.
483 They are also reused if the \l reuseItems property is set to \c true. You
484 should therefore avoid storing state information in the delegates.
485
486 \sa {Row heights and column widths}, {Reusing items}, {Required Properties}
487*/
488
489/*!
490 \qmlproperty bool QtQuick::TableView::reuseItems
491
492 This property holds whether or not items instantiated from the \l delegate
493 should be reused. If set to \c false, any currently pooled items
494 are destroyed.
495
496 \sa {Reusing items}, TableView::pooled, TableView::reused
497*/
498
499/*!
500 \qmlproperty real QtQuick::TableView::contentWidth
501
502 This property holds the table width required to accommodate the number of
503 columns in the model. This is usually not the same as the \c width of the
504 \l view, which means that the table's width could be larger or smaller than
505 the viewport width. As a TableView cannot always know the exact width of
506 the table without loading all columns in the model, the \c contentWidth is
507 usually an estimate based on the initially loaded table.
508
509 If you know what the width of the table will be, assign a value to
510 \c contentWidth, to avoid unnecessary calculations and updates to the
511 TableView.
512
513 \sa contentHeight, columnWidthProvider
514*/
515
516/*!
517 \qmlproperty real QtQuick::TableView::contentHeight
518
519 This property holds the table height required to accommodate the number of
520 rows in the data model. This is usually not the same as the \c height of the
521 \c view, which means that the table's height could be larger or smaller than the
522 viewport height. As a TableView cannot always know the exact height of the
523 table without loading all rows in the model, the \c contentHeight is
524 usually an estimate based on the initially loaded table.
525
526 If you know what the height of the table will be, assign a
527 value to \c contentHeight, to avoid unnecessary calculations and updates to
528 the TableView.
529
530 \sa contentWidth, rowHeightProvider
531*/
532
533/*!
534 \qmlmethod QtQuick::TableView::forceLayout
535
536 Responding to changes in the model are batched so that they are handled
537 only once per frame. This means the TableView delays showing any changes
538 while a script is being run. The same is also true when changing
539 properties, such as \l rowSpacing or \l{Item::anchors.leftMargin}{leftMargin}.
540
541 This method forces the TableView to immediately update the layout so
542 that any recent changes take effect.
543
544 Calling this function re-evaluates the size and position of each visible
545 row and column. This is needed if the functions assigned to
546 \l rowHeightProvider or \l columnWidthProvider return different values than
547 what is already assigned.
548*/
549
550/*!
551 \qmlproperty bool QtQuick::TableView::alternatingRows
552
553 This property controls whether the background color of the rows should alternate.
554 The default value is style dependent.
555
556 \note This property is only a hint, and might therefore not be
557 respected by custom delegates. It's recommended that a delegate alternates
558 between \c palette.base and \c palette.alternateBase when this hint is
559 \c true, so that the colors can be set from outside of the delegate.
560 For example:
561
562 \code
563 background: Rectangle {
564 color: control.row === control.tableView.currentRow
565 ? control.palette.highlight
566 : (control.tableView.alternatingRows && control.row % 2 !== 0
567 ? control.palette.alternateBase
568 : control.palette.base)
569 }
570 \endcode
571*/
572
573/*!
574 \qmlproperty int QtQuick::TableView::leftColumn
575
576 This property holds the leftmost column that is currently visible inside the view.
577
578 \sa rightColumn, topRow, bottomRow
579*/
580
581/*!
582 \qmlproperty int QtQuick::TableView::rightColumn
583
584 This property holds the rightmost column that is currently visible inside the view.
585
586 \sa leftColumn, topRow, bottomRow
587*/
588
589/*!
590 \qmlproperty int QtQuick::TableView::topRow
591
592 This property holds the topmost row that is currently visible inside the view.
593
594 \sa leftColumn, rightColumn, bottomRow
595*/
596
597/*!
598 \qmlproperty int QtQuick::TableView::bottomRow
599
600 This property holds the bottom-most row that is currently visible inside the view.
601
602 \sa leftColumn, rightColumn, topRow
603*/
604
605/*!
606 \qmlproperty int QtQuick::TableView::currentColumn
607 \readonly
608
609 This read-only property holds the column in the view that contains the
610 item that is \l {Keyboard navigation}{current.} If no item is current, it will be \c -1.
611
612 \note In order for TableView to report what the current column is, you
613 need to assign an \l ItemSelectionModel to \l selectionModel.
614
615 \sa currentRow, selectionModel, {Selecting items}
616*/
617
618/*!
619 \qmlproperty int QtQuick::TableView::currentRow
620 \readonly
621
622 This read-only property holds the row in the view that contains the item
623 that is \l {Keyboard navigation}{current.} If no item is current, it will be \c -1.
624
625 \note In order for TableView to report what the current row is, you
626 need to assign an \l ItemSelectionModel to \l selectionModel.
627
628 \sa currentColumn, selectionModel, {Selecting items}
629*/
630
631/*!
632 \qmlproperty ItemSelectionModel QtQuick::TableView::selectionModel
633 \since 6.2
634
635 This property can be set to control which delegate items should be shown as
636 selected, and which item should be shown as current. If the delegate has a
637 \c {required property bool selected} defined, TableView will keep it in sync
638 with the selection state of the corresponding model item in the selection model.
639 If the delegate has a \c {required property bool current} defined, TableView will
640 keep it in sync with selectionModel.currentIndex.
641
642 \sa {Selecting items}, SelectionRectangle, keyNavigationEnabled, pointerNavigationEnabled
643*/
644
645/*!
646 \qmlproperty bool QtQuick::TableView::animate
647 \since 6.4
648
649 This property can be set to control if TableView should animate the
650 \l {Flickable::}{contentItem} (\l {Flickable::}{contentX} and
651 \l {Flickable::}{contentY}). It is used by
652 \l positionViewAtCell(), and when navigating
653 \l {QItemSelectionModel::currentIndex}{the current index}
654 with the keyboard. The default value is \c true.
655
656 If set to \c false, any ongoing animation will immediately stop.
657
658 \note This property is only a hint. TableView might choose to position
659 the content item without an animation if, for example, the target cell is not
660 \l {isRowLoaded()}{loaded}. However, if set to \c false, animations will
661 always be off.
662
663 \sa positionViewAtCell()
664*/
665
666/*!
667 \qmlproperty bool QtQuick::TableView::keyNavigationEnabled
668 \since 6.4
669
670 This property can be set to control if the user should be able
671 to change \l {QItemSelectionModel::currentIndex()}{the current index}
672 using the keyboard. The default value is \c true.
673
674 \note In order for TableView to support keyboard navigation, you
675 need to assign an \l ItemSelectionModel to \l selectionModel.
676
677 \sa {Keyboard navigation}, selectionModel, selectionBehavior
678 \sa pointerNavigationEnabled, {Flickable::}{interactive}
679*/
680
681/*!
682 \qmlproperty bool QtQuick::TableView::pointerNavigationEnabled
683 \since 6.4
684
685 This property can be set to control if the user should be able
686 to change \l {QItemSelectionModel::currentIndex()}{the current index}
687 using mouse or touch. The default value is \c true.
688
689 \sa selectionModel, keyNavigationEnabled, {Flickable::}{interactive}
690*/
691
692/*!
693 \qmlproperty enumeration QtQuick::TableView::selectionBehavior
694 \since 6.4
695
696 This property holds whether the user can select cells, rows or columns.
697
698 \value TableView.SelectionDisabled
699 The user cannot perform selections
700 \value TableView.SelectCells
701 (Default value) The user can select individual cells
702 \value TableView.SelectRows
703 The user can only select rows
704 \value TableView.SelectColumns
705 The user can only select columns
706
707 \sa {Selecting items}, selectionMode, selectionModel, keyNavigationEnabled
708*/
709
710/*!
711 \qmlproperty enumeration QtQuick::TableView::selectionMode
712 \since 6.6
713
714 If \l selectionBehavior is set to \c {TableView.SelectCells}, this property holds
715 whether the user can select one cell at a time, or multiple cells.
716 If \l selectionBehavior is set to \c {TableView.SelectRows}, this property holds
717 whether the user can select one row at a time, or multiple rows.
718 If \l selectionBehavior is set to \c {TableView.SelectColumns}, this property holds
719 whether the user can select one column at a time, or multiple columns.
720
721 The following modes are available:
722
723 \value TableView.SingleSelection
724 The user can select a single cell, row or column.
725 \value TableView.ContiguousSelection
726 The user can select a single contiguous block of cells.
727 An existing selection can be made bigger or smaller by holding down
728 the \c Shift modifier while selecting.
729 \value TableView.ExtendedSelection
730 (Default value) The user can select multiple individual blocks of
731 cells. An existing selection can be made bigger or smaller by
732 holding down the \c Shift modifier while selecting. A new selection
733 block can be started without clearing the current selection by
734 holding down the \c Control modifier while selecting.
735
736 \sa {Selecting items}, selectionBehavior, selectionModel, keyNavigationEnabled
737*/
738
739/*!
740 \qmlproperty bool QtQuick::TableView::resizableColumns
741 \since 6.5
742
743 This property holds whether the user is allowed to resize columns
744 by dragging between the cells. The default value is \c false.
745*/
746
747/*!
748 \qmlproperty bool QtQuick::TableView::resizableRows
749 \since 6.5
750
751 This property holds whether the user is allowed to resize rows
752 by dragging between the cells. The default value is \c false.
753*/
754
755/*!
756 \qmlproperty enumeration QtQuick::TableView::editTriggers
757 \since 6.5
758
759 This property holds the different ways the user can start to edit a cell.
760 It can be a combination of the following values:
761
762 \default TableView.DoubleTapped | TableView.EditKeyPressed.
763 \value TableView.NoEditTriggers - the user cannot trigger editing of cells.
764 When this value is set, TableView will neither \e {open or close}
765 the edit delegate as a response to any user interaction.
766 But the application can call \l edit() and \l closeEditor() manually.
767 \value TableView.SingleTapped - the user can edit a cell by single tapping it.
768 \value TableView.DoubleTapped - the user can edit a cell by double tapping it.
769 \value TableView.SelectedTapped - the user can edit a
770 \l {QItemSelectionModel::selectedIndexes()}{selected cell} by tapping it.
771 \value TableView.EditKeyPressed - the user can edit the
772 \l {ItemSelectionModel::currentIndex}{current cell} by pressing one
773 of the edit keys. The edit keys are decided by the OS, but are normally
774 \c Qt::Key_Enter and \c Qt::Key_Return.
775 \value TableView.AnyKeyPressed - the user can edit the
776 \l {ItemSelectionModel::currentIndex}{current cell} by pressing any key, other
777 than the cell navigation keys. The pressed key is also sent to the
778 focus object inside the \l {TableView::editDelegate}{edit delegate}.
779
780 For \c TableView.SelectedTapped, \c TableView.EditKeyPressed, and
781 \c TableView.AnyKeyPressed to have any effect, TableView needs to have a
782 \l {selectionModel}{selection model} assigned, since they depend on a
783 \l {ItemSelectionModel::currentIndex}{current index} being set. To be
784 able to receive any key events at all, TableView will also need to have
785 \l QQuickItem::activeFocus.
786
787 When editing a cell, the user can press \c Qt::Key_Tab or \c Qt::Key_Backtab
788 to \l {TableView::commit}{commit} the data, and move editing to the next
789 cell. This behavior can be disabled by setting
790 \l QQuickItem::activeFocusOnTab on TableView to \c false.
791
792 \note In order for a cell to be editable, the \l delegate needs an
793 \l {TableView::editDelegate}{edit delegate} attached, and the model
794 needs to return \c Qt::ItemIsEditable from \l QAbstractItemModel::flags()
795 (exemplified underneath).
796 If you still cannot edit a cell after activating one of the specified
797 triggers, you can, as a help, try to call \l edit() explicitly (e.g
798 from a Button/TapHandler). Doing so will print out a warning explaining
799 why the cell cannot be edited.
800
801 \code
802 Qt::ItemFlags QAbstractItemModelSubClass::flags(const QModelIndex &index) const override
803 {
804 Q_UNUSED(index)
805 return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
806 }
807 \endcode
808
809 \sa TableView::editDelegate, TableView::commit, {Editing cells}
810*/
811
812/*!
813 \qmlmethod QtQuick::TableView::positionViewAtCell(point cell, PositionMode mode, point offset, rect subRect)
814
815 Positions \l {Flickable::}{contentX} and \l {Flickable::}{contentY} such
816 that \a cell is at the position specified by \a mode. \a mode
817 can be an or-ed combination of the following:
818
819 \value TableView.AlignLeft Position the cell at the left of the view.
820 \value TableView.AlignHCenter Position the cell at the horizontal center of the view.
821 \value TableView.AlignRight Position the cell at the right of the view.
822 \value TableView.AlignTop Position the cell at the top of the view.
823 \value TableView.AlignVCenter Position the cell at the vertical center of the view.
824 \value TableView.AlignBottom Position the cell at the bottom of the view.
825 \value TableView.AlignCenter The same as (TableView.AlignHCenter | TableView.AlignVCenter)
826 \value TableView.Visible If any part of the cell is visible then take no action. Otherwise
827 move the content item so that the entire cell becomes visible.
828 \value TableView.Contain If the entire cell is visible then take no action. Otherwise
829 move the content item so that the entire cell becomes visible. If the cell is
830 bigger than the view, the top-left part of the cell will be preferred.
831
832 If no vertical alignment is specified, vertical positioning will be ignored.
833 The same is true for horizontal alignment.
834
835 Optionally, you can specify \a offset to move \e contentX and \e contentY an extra number of
836 pixels beyond the target alignment. E.g if you want to position the view so
837 that cell [10, 10] ends up at the top-left corner with a 5px margin, you could do:
838
839 \code
840 positionViewAtCell(Qt.point(10, 10), TableView.AlignLeft | TableView.AlignTop, Qt.point(-5, -5))
841 \endcode
842
843 As of Qt 6.4, you can specify a \a subRect to position on a rectangle inside
844 the \a cell, rather than on the bounding rectangle of the whole cell. This can
845 be useful if the cell is e.g larger than the view, and you want to ensure that a
846 specific part of it is visible. The \a subRect needs to be
847 \l {QRectF::isValid()}{valid} to be taken into consideration.
848
849 \note It is not recommended to use \e contentX or \e contentY
850 to position the view at a particular cell. This is unreliable since removing items from
851 the start of the table does not cause all other items to be repositioned.
852 TableView can also sometimes place rows and columns at approximate positions to
853 optimize for speed. The only exception is if the cell is already visible in
854 the view, which can be checked upfront by calling \l itemAtCell().
855
856 Methods should only be called after the Component has completed. To position
857 the view at startup, this method should be called by Component.onCompleted. For
858 example, to position the view at the end:
859
860 \code
861 Component.onCompleted: positionViewAtCell(Qt.point(columns - 1, rows - 1), TableView.AlignRight | TableView.AlignBottom)
862 \endcode
863
864 \note The second argument to this function used to be Qt.Alignment. For backwards
865 compatibility, that enum can still be used. The change to use PositionMode was done
866 in Qt 6.4.
867
868 \sa animate
869*/
870
871/*!
872 \qmlmethod QtQuick::TableView::positionViewAtIndex(QModelIndex index, PositionMode mode, point offset, rect subRect)
873 \since 6.5
874
875 Positions the view such that \a index is at the position specified
876 by \a mode, \a offset and \a subRect.
877
878 Convenience method for calling
879 \code
880 positionViewAtRow(rowAtIndex(index), mode & Qt.AlignVertical_Mask, offset.y, subRect)
881 positionViewAtColumn(columnAtIndex(index), mode & Qt.AlignVertical_Mask, offset.x, subRect)
882 \endcode
883*/
884
885/*!
886 \qmlmethod bool QtQuick::TableView::isColumnLoaded(int column)
887 \since 6.2
888
889 Returns \c true if the given \a column is loaded.
890
891 A column is loaded when TableView has loaded the delegate items
892 needed to show the column inside the view. This also usually means
893 that the column is visible for the user, but not always.
894
895 This function can be used whenever you need to iterate over the
896 delegate items for a column, e.g from a \l columnWidthProvider, to
897 be sure that the delegate items are available for iteration.
898*/
899
900/*!
901 \qmlmethod bool QtQuick::TableView::isRowLoaded(int row)
902 \since 6.2
903
904 Returns \c true if the given \a row is loaded.
905
906 A row is loaded when TableView has loaded the delegate items
907 needed to show the row inside the view. This also usually means
908 that the row is visible for the user, but not always.
909
910 This function can be used whenever you need to iterate over the
911 delegate items for a row, e.g from a \l rowHeightProvider, to
912 be sure that the delegate items are available for iteration.
913*/
914
915/*!
916 \qmlmethod QtQuick::TableView::positionViewAtCell(int column, int row, PositionMode mode, point offset, rect subRect)
917 \deprecated
918
919 Use \l {positionViewAtIndex()}{positionViewAtIndex(index(row, column), ...)} instead.
920*/
921
922/*!
923 \qmlmethod QtQuick::TableView::positionViewAtRow(int row, PositionMode mode, real offset, rect subRect)
924
925 Positions {Flickable::}{contentY} such that \a row is at the position specified
926 by \a mode, \a offset and \a subRect.
927
928 Convenience method for calling
929 \code
930 positionViewAtCell(Qt.point(0, row), mode & Qt.AlignVertical_Mask, offset, subRect)
931 \endcode
932*/
933
934/*!
935 \qmlmethod QtQuick::TableView::positionViewAtColumn(int column, PositionMode mode, real offset, rect subRect)
936
937 Positions {Flickable::}{contentX} such that \a column is at the position specified
938 by \a mode, \a offset and \a subRect.
939
940 Convenience method for calling
941 \code
942 positionViewAtCell(Qt.point(column, 0), mode & Qt.AlignHorizontal_Mask, offset, subRect)
943 \endcode
944*/
945
946/*!
947 \qmlmethod Item QtQuick::TableView::itemAtCell(point cell)
948
949 Returns the delegate item at \a cell if loaded, otherwise \c null.
950
951 \note only the items that are visible in the view are normally loaded.
952 As soon as a cell is flicked out of the view, the item inside will
953 either be unloaded or placed in the recycle pool. As such, the return
954 value should never be stored.
955*/
956
957/*!
958 \qmlmethod Item QtQuick::TableView::itemAtCell(int column, int row)
959 \deprecated
960
961 Use \l {itemAtIndex()}{itemAtIndex(index(row, column))} instead.
962*/
963
964/*!
965 \qmlmethod Item QtQuick::TableView::itemAtIndex(QModelIndex index)
966 \since 6.5
967
968 Returns the instantiated delegate item for the cell that represents
969 \a index. If the item is not \l {isRowLoaded()}{loaded}, the value
970 will be \c null.
971
972 \note only the items that are visible in the view are normally loaded.
973 As soon as a cell is flicked out of the view, the item inside will
974 either be unloaded or placed in the recycle pool. As such, the return
975 value should never be stored.
976
977 \note If the \l model is not a QAbstractItemModel, you can also use
978 \l {itemAtCell()}{itemAtCell(Qt.point(column, row))}. But be aware
979 that \c {point.x} maps to columns and \c {point.y} maps to rows.
980*/
981
982/*!
983 \qmlmethod Point QtQuick::TableView::cellAtPos(point position, bool includeSpacing)
984 \obsolete
985
986 Use cellAtPosition(point position) instead.
987*/
988
989/*!
990 \qmlmethod Point QtQuick::TableView::cellAtPos(real x, real y, bool includeSpacing)
991 \obsolete
992
993 Use cellAtPosition(real x, real y) instead.
994*/
995
996/*!
997 \qmlmethod Point QtQuick::TableView::cellAtPosition(point position, bool includeSpacing)
998
999 Returns the cell at the given \a position in the table. \a position should be relative
1000 to the \l {Flickable::}{contentItem}. If no \l {isRowLoaded()}{loaded} cell intersects
1001 with \a position, the return value will be \c point(-1, -1).
1002
1003 If \a includeSpacing is set to \c true, a cell's bounding box will be considered
1004 to include half the adjacent \l rowSpacing and \l columnSpacing on each side. The
1005 default value is \c false.
1006
1007 \note A \l {Qt Quick Input Handlers}{Input Handler} attached to a TableView installs
1008 itself on the \l {Flickable::}{contentItem} rather than the view. So the position
1009 reported by the handler can be used directly in a call to this function without any
1010 \l {QQuickItem::mapFromItem()}{mapping}.
1011
1012 \sa columnSpacing, rowSpacing
1013*/
1014
1015/*!
1016 \qmlmethod Point QtQuick::TableView::cellAtPosition(real x, real y, bool includeSpacing)
1017
1018 Convenience for calling \c{cellAtPosition(Qt.point(x, y), includeSpacing)}.
1019*/
1020
1021/*!
1022 \qmlmethod real QtQuick::TableView::columnWidth(int column)
1023 \since 6.2
1024
1025 Returns the width of the given \a column. If the column is not
1026 loaded (and therefore not visible), the return value will be \c -1.
1027
1028 \sa columnWidthProvider, implicitColumnWidth(), isColumnLoaded(), {Row heights and column widths}
1029*/
1030
1031/*!
1032 \qmlmethod real QtQuick::TableView::rowHeight(int row)
1033 \since 6.2
1034
1035 Returns the height of the given \a row. If the row is not
1036 loaded (and therefore not visible), the return value will be \c -1.
1037
1038 \sa rowHeightProvider, implicitRowHeight(), isRowLoaded(), {Row heights and column widths}
1039*/
1040
1041/*!
1042 \qmlmethod real QtQuick::TableView::implicitColumnWidth(int column)
1043 \since 6.2
1044
1045 Returns the implicit width of the given \a column. If the
1046 column is not loaded (and therefore not visible), the return value
1047 will be \c -1.
1048
1049 The implicit width of a column is the largest implicitWidth
1050 found among the currently loaded delegate items inside that column.
1051 Widths returned by the \l columnWidthProvider will not be taken
1052 into account.
1053
1054 \sa columnWidthProvider, columnWidth(), isColumnLoaded(), {Row heights and column widths}
1055*/
1056
1057/*!
1058 \qmlmethod real QtQuick::TableView::implicitRowHeight(int row)
1059 \since 6.2
1060
1061 Returns the implicit height of the given \a row. If the
1062 row is not loaded (and therefore not visible), the return value
1063 will be \c -1.
1064
1065 The implicit height of a row is the largest implicitHeight
1066 found among the currently loaded delegate items inside that row.
1067 Heights returned by the \l rowHeightProvider will not be taken
1068 into account.
1069
1070 \sa rowHeightProvider, rowHeight(), isRowLoaded(), {Row heights and column widths}
1071*/
1072
1073/*!
1074 \qmlmethod QtQuick::TableView::setColumnWidth(int column, real size)
1075
1076 Sets the explicit column width of column \a column to \a size.
1077
1078 If you want to read back the values you set with this function, you
1079 should use \l explicitColumnWidth(). \l columnWidth() will return
1080 the actual size of the column, which can be different if a
1081 \l columnWidthProvider is set.
1082
1083 When TableView needs to resolve the width of \a column, it will first try
1084 to call the \l columnWidthProvider. Only if a provider is not set, will
1085 the widths set with this function be used by default. You can, however, call
1086 \l explicitColumnWidth() from within the provider, and if needed, moderate
1087 the values to e.g always be within a certain interval.
1088 The following snippet shows an example on how to do that:
1089
1090 \code
1091 columnWidthProvider: function(column) {
1092 let w = explicitColumnWidth(column)
1093 if (w >= 0)
1094 return Math.max(100, w);
1095 return implicitColumnWidth(column)
1096 }
1097 \endcode
1098
1099 If \a size is equal to \c 0, the column will be hidden. If \a size is
1100 equal to \c -1, the column will be reset back to use \l implicitColumnWidth().
1101 You are allowed to specify column sizes for columns that are outside the
1102 size of the model.
1103
1104 \note The sizes you set will not be cleared if you change the \l model.
1105 To clear the sizes, you need to call \l clearColumnWidths() explicitly.
1106
1107 \include tableview.qdocinc explicit-column-size-and-syncview
1108
1109 \note For models with \e lots of columns, using \l setColumnWidth() to set the widths for
1110 all the columns at start-up, can be suboptimal. This will consume start-up time and
1111 memory (for storing all the widths). A more scalable approach is to use a
1112 \l columnWidthProvider instead, or rely on the implicit width of the delegate.
1113 A \c columnWidthProvider will only be called on an as-needed basis, and will not
1114 be affected by the size of the model.
1115
1116 \sa explicitColumnWidth(), setRowHeight(), clearColumnWidths(), {Row heights and column widths}
1117*/
1118
1119/*!
1120 \qmlmethod QtQuick::TableView::clearColumnWidths()
1121
1122 Clears all the column widths set with \l setColumnWidth().
1123
1124 \include tableview.qdocinc explicit-column-size-and-syncview
1125
1126 \sa setColumnWidth(), clearRowHeights(), {Row heights and column widths}
1127*/
1128
1129/*!
1130 \qmlmethod qreal QtQuick::TableView::explicitColumnWidth(int column)
1131
1132 Returns the width of the \a column set with \l setColumnWidth(). This width might
1133 differ from the actual width of the column, if a \l columnWidthProvider
1134 is in use. To get the actual width of a column, use \l columnWidth().
1135
1136 A return value equal to \c 0 means that the column has been told to hide.
1137 A return value equal to \c -1 means that no explicit width has been set
1138 for the column.
1139
1140 \include tableview.qdocinc explicit-column-size-and-syncview
1141
1142 \sa setColumnWidth(), columnWidth(), {Row heights and column widths}
1143*/
1144
1145/*!
1146 \qmlmethod QtQuick::TableView::setRowHeight(int row, real size)
1147
1148 Sets the explicit row height of row \a row to \a size.
1149
1150 If you want to read back the values you set with this function, you
1151 should use \l explicitRowHeight(). \l rowHeight() will return
1152 the actual height of the row, which can be different if a
1153 \l rowHeightProvider is set.
1154
1155 When TableView needs to resolve the height of \a row, it will first try
1156 to call the \l rowHeightProvider. Only if a provider is not set, will
1157 the heights set with this function be used by default. You can, however, call
1158 \l explicitRowHeight() from within the provider, and if needed, moderate
1159 the values to e.g always be within a certain interval.
1160 The following snippet shows an example on how to do that:
1161
1162 \code
1163 rowHeightProvider: function(row) {
1164 let h = explicitRowHeight(row)
1165 if (h >= 0)
1166 return Math.max(100, h);
1167 return implicitRowHeight(row)
1168 }
1169 \endcode
1170
1171 If \a size is equal to \c 0, the row will be hidden. If \a size is
1172 equal to \c -1, the row will be reset back to use \l implicitRowHeight().
1173 You are allowed to specify row sizes for rows that are outside the
1174 size of the model.
1175
1176 \note The sizes you set will not be cleared if you change the \l model.
1177 To clear the sizes, you need to call \l clearRowHeights() explicitly.
1178
1179 \include tableview.qdocinc explicit-row-size-and-syncview
1180
1181 \note For models with \e lots of rows, using \l setRowHeight() to set the heights for
1182 all the rows at start-up, can be suboptimal. This will consume start-up time and
1183 memory (for storing all the heights). A more scalable approach is to use a
1184 \l rowHeightProvider instead, or rely on the implicit height of the delegate.
1185 A \c rowHeightProvider will only be called on an as-needed basis, and will not
1186 be affected by the size of the model.
1187
1188 \sa explicitRowHeight(), setColumnWidth(), {Row heights and column widths}
1189*/
1190
1191/*!
1192 \qmlmethod QtQuick::TableView::clearRowHeights()
1193
1194 Clears all the row heights set with \l setRowHeight().
1195
1196 \include tableview.qdocinc explicit-row-size-and-syncview
1197
1198 \sa setRowHeight(), clearColumnWidths(), {Row heights and column widths}
1199*/
1200
1201/*!
1202 \qmlmethod qreal QtQuick::TableView::explicitRowHeight(int row)
1203
1204 Returns the height of the \a row set with \l setRowHeight(). This height might
1205 differ from the actual height of the column, if a \l rowHeightProvider
1206 is in use. To get the actual height of a row, use \l rowHeight().
1207
1208 A return value equal to \c 0 means that the row has been told to hide.
1209 A return value equal to \c -1 means that no explicit height has been set
1210 for the row.
1211
1212 \include tableview.qdocinc explicit-row-size-and-syncview
1213
1214 \sa setRowHeight(), rowHeight(), {Row heights and column widths}
1215*/
1216
1217/*!
1218 \qmlmethod QModelIndex QtQuick::TableView::modelIndex(int row, int column)
1219 \since 6.4
1220 \deprecated
1221
1222 Use \l {QtQuick::TableView::}{index(int row, int column)} instead.
1223
1224 \note Because of an API incompatible change between Qt 6.4.0 and Qt 6.4.2, the
1225 order of \c row and \c column was specified in the opposite order. If you
1226 rely on the order to be \c {modelIndex(column, row)}, you can set the
1227 environment variable \c QT_QUICK_TABLEVIEW_COMPAT_VERSION to \c 6.4
1228*/
1229
1230/*!
1231 \qmlmethod QModelIndex QtQuick::TableView::modelIndex(point cell)
1232 \since 6.4
1233
1234 Convenience function for doing:
1235 \code
1236 modelIndex(cell.y, cell.x)
1237 \endcode
1238
1239 A cell is simply a \l point that combines row and column into
1240 a single type.
1241
1242 \note \c {point.x} will map to the column, and \c {point.y} will map to the row.
1243*/
1244
1245/*!
1246 \qmlmethod QModelIndex QtQuick::TableView::index(int row, int column)
1247 \since 6.4.3
1248
1249 Returns the \l QModelIndex that maps to \a row and \a column in the view.
1250
1251 \a row and \a column should be the row and column in the view (table row and
1252 table column), and not a row and column in the model. For a plain
1253 TableView, this is equivalent of calling \c {model.index(row, column).}
1254 But for a subclass of TableView, like TreeView, where the data model is
1255 wrapped inside an internal proxy model that flattens the tree structure
1256 into a table, you need to use this function to resolve the model index.
1257
1258 \sa rowAtIndex(), columnAtIndex()
1259*/
1260
1261/*!
1262 \qmlmethod int QtQuick::TableView::rowAtIndex(QModelIndex modelIndex)
1263 \since 6.4
1264
1265 Returns the row in the view that maps to \a modelIndex in the model.
1266
1267 \sa columnAtIndex(), index()
1268*/
1269
1270/*!
1271 \qmlmethod int QtQuick::TableView::columnAtIndex(QModelIndex modelIndex)
1272 \since 6.4
1273
1274 Returns the column in the view that maps to \a modelIndex in the model.
1275
1276 \sa rowAtIndex(), index()
1277*/
1278
1279/*!
1280 \qmlmethod point QtQuick::TableView::cellAtIndex(QModelIndex modelIndex)
1281 \since 6.4
1282
1283 Returns the cell in the view that maps to \a modelIndex in the model.
1284 Convenience function for doing:
1285
1286 \code
1287 Qt.point(columnAtIndex(modelIndex), rowAtIndex(modelIndex))
1288 \endcode
1289
1290 A cell is simply a \l point that combines row and column into
1291 a single type.
1292
1293 \note that \c {point.x} will map to the column, and
1294 \c {point.y} will map to the row.
1295*/
1296
1297/*!
1298 \qmlmethod QtQuick::TableView::edit(QModelIndex modelIndex)
1299 \since 6.5
1300
1301 This function starts an editing session for the cell that represents
1302 \a modelIndex. If the user is already editing another cell, that session ends.
1303
1304 Normally you can specify the different ways of starting an edit session by
1305 using \l editTriggers instead. If that isn't sufficient, you can use this
1306 function. To take full control over cell editing and keep TableView from
1307 interfering, set editTriggers to \c TableView.NoEditTriggers.
1308
1309 \note The \l {ItemSelectionModel::currentIndex}{current index} in the
1310 \l {selectionModel}{selection model} will also change to \a modelIndex.
1311
1312 \sa closeEditor(), editTriggers, TableView::editDelegate, {Editing cells}
1313*/
1314
1315/*!
1316 \qmlmethod QtQuick::TableView::closeEditor()
1317 \since 6.5
1318
1319 If the user is editing a cell, calling this function will
1320 stop the editing, and destroy the edit delegate instance.
1321
1322 \sa edit(), TableView::editDelegate, {Editing cells}
1323*/
1324
1325/*!
1326 \qmlsignal QtQuick::TableView::layoutChanged()
1327 \since 6.5
1328
1329 This signal is emitted whenever the layout of the
1330 \l {isColumnLoaded()}{loaded} rows and columns has potentially
1331 changed. This will especially be the case when \l forceLayout()
1332 is called, but also when e.g resizing a row or a column, or
1333 when a row or column have entered or left the viewport.
1334
1335 This signal can be used to for example update the geometry
1336 of overlays.
1337
1338 \sa forceLayout(), {Overlays and underlays}
1339*/
1340
1341/*!
1342 \qmlattachedproperty TableView QtQuick::TableView::view
1343
1344 This attached property holds the view that manages the delegate instance.
1345 It is attached to each instance of the delegate.
1346*/
1347
1348/*!
1349 \qmlattachedsignal QtQuick::TableView::pooled
1350
1351 This signal is emitted after an item has been added to the reuse
1352 pool. You can use it to pause ongoing timers or animations inside
1353 the item, or free up resources that cannot be reused.
1354
1355 This signal is emitted only if the \l reuseItems property is \c true.
1356
1357 \sa {Reusing items}, reuseItems, reused
1358*/
1359
1360/*!
1361 \qmlattachedsignal QtQuick::TableView::reused
1362
1363 This signal is emitted after an item has been reused. At this point, the
1364 item has been taken out of the pool and placed inside the content view,
1365 and the model properties such as index, row, and column have been updated.
1366
1367 Other properties that are not provided by the model does not change when an
1368 item is reused. You should avoid storing any state inside a delegate, but if
1369 you do, manually reset that state on receiving this signal.
1370
1371 This signal is emitted when the item is reused, and not the first time the
1372 item is created.
1373
1374 This signal is emitted only if the \l reuseItems property is \c true.
1375
1376 \sa {Reusing items}, reuseItems, pooled
1377*/
1378
1379/*!
1380 \qmlattachedsignal QtQuick::TableView::commit
1381 This signal is emitted by the \l {TableView::editDelegate}{edit delegate}
1382
1383 This attached signal is emitted when the \l {TableView::editDelegate}{edit delegate}
1384 is active, and the user presses \l Qt::Key_Enter or \l Qt::Key_Return. It will also
1385 be emitted if TableView has \l QQuickItem::activeFocusOnTab set, and the user
1386 presses Qt::Key_Tab or Qt::Key_Backtab.
1387
1388 This signal will \e not be emitted if editing ends because of reasons other
1389 than the ones mentioned. This includes e.g if the user presses
1390 Qt::Key_Escape, taps outside the delegate, the row or column being
1391 edited is deleted, or if the application calls \l closeEditor().
1392
1393 Upon receiving the signal, the edit delegate should write any modified data
1394 back to the model.
1395
1396 \note This property should be attached to the
1397 \l {TableView::editDelegate}{edit delegate}, and not to the \l delegate.
1398
1399 \sa TableView::editDelegate, editTriggers, {Editing cells}
1400*/
1401
1402/*!
1403 \qmlattachedproperty Component QtQuick::TableView::editDelegate
1404
1405 This attached property holds the edit delegate. It's instantiated
1406 when editing begins, and parented to the delegate it edits. It
1407 supports the same required properties as the
1408 \l {TableView::delegate}{TableView delegate}, including \c index, \c row and \c column.
1409 Properties of the model, like \c display and \c edit, are also available
1410 (depending on the \l {QAbstractItemModel::roleNames()}{role names} exposed
1411 by the model).
1412
1413 Editing starts when the actions specified by \l editTriggers are met, and
1414 the current cell is editable.
1415
1416 \note In order for a cell to be editable, the model needs to override
1417 \l QAbstractItemModel::flags(), and return \c Qt::ItemIsEditable.
1418
1419 You can also open and close the edit delegate manually by calling \l edit()
1420 and \l closeEditor(), respectively. The \c Qt::ItemIsEditable flag will
1421 then be ignored.
1422
1423 Editing ends when the user presses \c Qt::Key_Enter or \c Qt::Key_Return
1424 (and also \c Qt::Key_Tab or \c Qt::Key_Backtab, if TableView has
1425 \l QQuickItem::activeFocusOnTab set). In that case, the \l TableView::commit
1426 signal will be emitted, so that the edit delegate can respond by writing any
1427 modified data back to the model. If editing ends because of other reasons
1428 (e.g if the user presses Qt::Key_Escape), the signal will not be emitted.
1429 In any case will \l {Component::destruction}{destruction()} be emitted in the end.
1430
1431 While the edit delegate is showing, the cell underneath will still be visible, and
1432 therefore shine through if the edit delegate is translucent, or otherwise doesn't
1433 cover the whole cell. If this is not wanted, you can either let the root item
1434 of the edit delegate be a solid \l Rectangle, or hide some of the items
1435 inside the \l {TableView::delegate}{TableView delegate.}. The latter can be done
1436 by defining a property \c {required property bool editing} inside it, that you
1437 bind to the \l {QQuickItem::}{visible} property of some of the child items.
1438 The following snippet shows how to do that:
1439
1440 \snippet qml/tableview/editdelegate.qml 1
1441
1442 When the edit delegate is instantiated, TableView will call \l QQuickItem::forceActiveFocus()
1443 on it. If you want active focus to be set on a child of the edit delegate instead, let
1444 the edit delegate be a \l FocusScope.
1445
1446 \sa editTriggers, TableView::commit, edit(), closeEditor(), {Editing cells}
1447*/
1448
1449QT_BEGIN_NAMESPACE
1450
1451Q_LOGGING_CATEGORY(lcTableViewDelegateLifecycle, "qt.quick.tableview.lifecycle")
1452
1453#define Q_TABLEVIEW_UNREACHABLE(output) { dumpTable(); qWarning() << "output:" << output; Q_UNREACHABLE(); }
1454#define Q_TABLEVIEW_ASSERT(cond, output) Q_ASSERT((cond) || [&](){ dumpTable(); qWarning() << "output:" << output; return false;}())
1455
1456static const Qt::Edge allTableEdges[] = { Qt::LeftEdge, Qt::RightEdge, Qt::TopEdge, Qt::BottomEdge };
1457
1458static const char* kRequiredProperties = "_qt_tableview_requiredpropertymask";
1459static const char* kRequiredProperty_selected = "selected";
1460static const char* kRequiredProperty_current = "current";
1461static const char* kRequiredProperty_editing = "editing";
1462
1463QQuickTableViewPrivate::EdgeRange::EdgeRange()
1464 : startIndex(kEdgeIndexNotSet)
1465 , endIndex(kEdgeIndexNotSet)
1466 , size(0)
1467{}
1468
1469bool QQuickTableViewPrivate::EdgeRange::containsIndex(Qt::Edge edge, int index)
1470{
1471 if (startIndex == kEdgeIndexNotSet)
1472 return false;
1473
1474 if (endIndex == kEdgeIndexAtEnd) {
1475 switch (edge) {
1476 case Qt::LeftEdge:
1477 case Qt::TopEdge:
1478 return index <= startIndex;
1479 case Qt::RightEdge:
1480 case Qt::BottomEdge:
1481 return index >= startIndex;
1482 }
1483 }
1484
1485 const int s = std::min(a: startIndex, b: endIndex);
1486 const int e = std::max(a: startIndex, b: endIndex);
1487 return index >= s && index <= e;
1488}
1489
1490QQuickTableViewPrivate::QQuickTableViewPrivate()
1491 : QQuickFlickablePrivate()
1492{
1493}
1494
1495QQuickTableViewPrivate::~QQuickTableViewPrivate()
1496{
1497 if (editItem) {
1498 QQuickItem *cellItem = editItem->parentItem();
1499 Q_ASSERT(cellItem);
1500 editModel->dispose(object: editItem);
1501 tableModel->release(object: cellItem, reusable: QQmlInstanceModel::NotReusable);
1502 }
1503
1504 if (editModel)
1505 delete editModel;
1506
1507 for (auto *fxTableItem : loadedItems) {
1508 if (auto item = fxTableItem->item) {
1509 if (fxTableItem->ownItem)
1510 delete item;
1511 else if (tableModel)
1512 tableModel->dispose(object: item);
1513 }
1514 delete fxTableItem;
1515 }
1516
1517 if (tableModel)
1518 delete tableModel;
1519}
1520
1521QString QQuickTableViewPrivate::tableLayoutToString() const
1522{
1523 if (loadedItems.isEmpty())
1524 return QLatin1String("table is empty!");
1525 return QString(QLatin1String("table cells: (%1,%2) -> (%3,%4), item count: %5, table rect: %6,%7 x %8,%9"))
1526 .arg(a: leftColumn()).arg(a: topRow())
1527 .arg(a: rightColumn()).arg(a: bottomRow())
1528 .arg(a: loadedItems.size())
1529 .arg(a: loadedTableOuterRect.x())
1530 .arg(a: loadedTableOuterRect.y())
1531 .arg(a: loadedTableOuterRect.width())
1532 .arg(a: loadedTableOuterRect.height());
1533}
1534
1535void QQuickTableViewPrivate::dumpTable() const
1536{
1537 auto listCopy = loadedItems.values();
1538 std::stable_sort(first: listCopy.begin(), last: listCopy.end(),
1539 comp: [](const FxTableItem *lhs, const FxTableItem *rhs)
1540 { return lhs->index < rhs->index; });
1541
1542 qWarning() << QStringLiteral("******* TABLE DUMP *******");
1543 for (int i = 0; i < listCopy.size(); ++i)
1544 qWarning() << static_cast<FxTableItem *>(listCopy.at(i))->cell;
1545 qWarning() << tableLayoutToString();
1546
1547 const QString filename = QStringLiteral("QQuickTableView_dumptable_capture.png");
1548 const QString path = QDir::current().absoluteFilePath(fileName: filename);
1549 if (q_func()->window() && q_func()->window()->grabWindow().save(fileName: path))
1550 qWarning() << "Window capture saved to:" << path;
1551}
1552
1553void QQuickTableViewPrivate::setRequiredProperty(const char *property,
1554 const QVariant &value, int serializedModelIndex, QObject *object, bool init)
1555{
1556 Q_Q(QQuickTableView);
1557
1558 QQmlTableInstanceModel *tableInstanceModel = qobject_cast<QQmlTableInstanceModel *>(object: model);
1559 if (!tableInstanceModel) {
1560 // TableView only supports using required properties when backed by
1561 // a QQmlTableInstanceModel. This is almost always the case, except
1562 // if you assign it an ObjectModel or a DelegateModel (which are really
1563 // not supported by TableView, it expects a QAIM).
1564 return;
1565 }
1566
1567 // Attaching a property list to the delegate item is just a
1568 // work-around until QMetaProperty::isRequired() works (QTBUG-98846).
1569 const QString propertyName = QString::fromUtf8(utf8: property);
1570
1571 if (init) {
1572 bool wasRequired = false;
1573 if (object == editItem) {
1574 // Special case: the item that we should write to belongs to the edit
1575 // model rather than 'model' (which is used for normal delegate items).
1576 wasRequired = editModel->setRequiredProperty(index: serializedModelIndex, name: propertyName, value);
1577 } else {
1578 wasRequired = tableInstanceModel->setRequiredProperty(index: serializedModelIndex, name: propertyName, value);
1579 }
1580 if (wasRequired) {
1581 QStringList propertyList = object->property(name: kRequiredProperties).toStringList();
1582 object->setProperty(name: kRequiredProperties, value: propertyList << propertyName);
1583 }
1584 } else {
1585 {
1586 const QStringList propertyList = object->property(name: kRequiredProperties).toStringList();
1587 if (propertyList.contains(str: propertyName)) {
1588 const auto metaObject = object->metaObject();
1589 const int propertyIndex = metaObject->indexOfProperty(name: property);
1590 const auto metaProperty = metaObject->property(index: propertyIndex);
1591 metaProperty.write(obj: object, value);
1592 }
1593 }
1594
1595 if (editItem) {
1596 // Whenever we're told to update a required property for a table item that has the
1597 // same model index as the edit item, we also mirror that update to the edit item.
1598 // As such, this function is never called for the edit item directly (except the
1599 // first time when it needs to be initialized).
1600 Q_TABLEVIEW_ASSERT(object != editItem, "");
1601 const QModelIndex modelIndex = q->modelIndex(cell: cellAtModelIndex(modelIndex: serializedModelIndex));
1602 if (modelIndex == editIndex) {
1603 const QStringList propertyList = editItem->property(name: kRequiredProperties).toStringList();
1604 if (propertyList.contains(str: propertyName)) {
1605 const auto metaObject = editItem->metaObject();
1606 const int propertyIndex = metaObject->indexOfProperty(name: property);
1607 const auto metaProperty = metaObject->property(index: propertyIndex);
1608 metaProperty.write(obj: editItem, value);
1609 }
1610 }
1611 }
1612
1613 }
1614}
1615
1616QQuickItem *QQuickTableViewPrivate::selectionPointerHandlerTarget() const
1617{
1618 return const_cast<QQuickTableView *>(q_func())->contentItem();
1619}
1620
1621bool QQuickTableViewPrivate::startSelection(const QPointF &pos)
1622{
1623 Q_Q(QQuickTableView);
1624 Q_UNUSED(pos);
1625
1626 if (selectionBehavior == QQuickTableView::SelectionDisabled) {
1627 qmlWarning(me: q) << "Cannot start selection: TableView.selectionBehavior == TableView.SelectionDisabled";
1628 return false;
1629 }
1630
1631 // Only allow a selection if it doesn't conflict with resizing
1632 if (resizeHandler->state() != QQuickTableViewResizeHandler::Listening)
1633 return false;
1634
1635 // For SingleSelection and ContiguousSelection, we should only allow one selection at a time
1636 if (selectionMode == QQuickTableView::SingleSelection
1637 || selectionMode == QQuickTableView::ContiguousSelection)
1638 clearSelection();
1639
1640 selectionStartCell = QPoint(-1, -1);
1641 selectionEndCell = QPoint(-1, -1);
1642 q->closeEditor();
1643 return true;
1644}
1645
1646void QQuickTableViewPrivate::setSelectionStartPos(const QPointF &pos)
1647{
1648 if (loadedItems.isEmpty())
1649 return;
1650 if (!selectionModel) {
1651 if (warnNoSelectionModel)
1652 qmlWarning(me: q_func()) << "Cannot set selection: no SelectionModel assigned!";
1653 warnNoSelectionModel = false;
1654 return;
1655 }
1656 const QAbstractItemModel *qaim = selectionModel->model();
1657 if (!qaim)
1658 return;
1659
1660 if (selectionMode == QQuickTableView::SingleSelection
1661 && cellIsValid(cell: selectionStartCell)) {
1662 return;
1663 }
1664
1665 const QRect prevSelection = selection();
1666 const QPoint clampedCell = clampedCellAtPos(pos);
1667 if (!cellIsValid(cell: clampedCell))
1668 return;
1669
1670 setCurrentIndex(clampedCell);
1671
1672 switch (selectionBehavior) {
1673 case QQuickTableView::SelectCells:
1674 selectionStartCell = clampedCell;
1675 break;
1676 case QQuickTableView::SelectRows:
1677 selectionStartCell = QPoint(0, clampedCell.y());
1678 break;
1679 case QQuickTableView::SelectColumns:
1680 selectionStartCell = QPoint(clampedCell.x(), 0);
1681 break;
1682 case QQuickTableView::SelectionDisabled:
1683 return;
1684 }
1685
1686 if (!cellIsValid(cell: selectionEndCell))
1687 return;
1688
1689 // Update selection model
1690 updateSelection(oldSelection: prevSelection, newSelection: selection());
1691}
1692
1693void QQuickTableViewPrivate::setSelectionEndPos(const QPointF &pos)
1694{
1695 if (loadedItems.isEmpty())
1696 return;
1697 if (!selectionModel) {
1698 if (warnNoSelectionModel)
1699 qmlWarning(me: q_func()) << "Cannot set selection: no SelectionModel assigned!";
1700 warnNoSelectionModel = false;
1701 return;
1702 }
1703 const QAbstractItemModel *qaim = selectionModel->model();
1704 if (!qaim)
1705 return;
1706
1707 const QRect prevSelection = selection();
1708
1709 QPoint clampedCell;
1710 if (selectionMode == QQuickTableView::SingleSelection) {
1711 clampedCell = selectionStartCell;
1712 } else {
1713 clampedCell = clampedCellAtPos(pos);
1714 if (!cellIsValid(cell: clampedCell))
1715 return;
1716 }
1717
1718 setCurrentIndex(clampedCell);
1719
1720 switch (selectionBehavior) {
1721 case QQuickTableView::SelectCells:
1722 selectionEndCell = clampedCell;
1723 break;
1724 case QQuickTableView::SelectRows:
1725 selectionEndCell = QPoint(tableSize.width() - 1, clampedCell.y());
1726 break;
1727 case QQuickTableView::SelectColumns:
1728 selectionEndCell = QPoint(clampedCell.x(), tableSize.height() - 1);
1729 break;
1730 case QQuickTableView::SelectionDisabled:
1731 return;
1732 }
1733
1734 if (!cellIsValid(cell: selectionStartCell))
1735 return;
1736
1737 // Update selection model
1738 updateSelection(oldSelection: prevSelection, newSelection: selection());
1739}
1740
1741QPoint QQuickTableViewPrivate::clampedCellAtPos(const QPointF &pos) const
1742{
1743 Q_Q(const QQuickTableView);
1744
1745 // Note: pos should be relative to selectionPointerHandlerTarget()
1746 QPoint cell = q->cellAtPosition(position: pos, includeSpacing: true);
1747 if (cellIsValid(cell))
1748 return cell;
1749
1750 // Clamp the cell to the loaded table and the viewport, whichever is the smallest
1751 QPointF clampedPos(
1752 qBound(min: loadedTableOuterRect.x(), val: pos.x(), max: loadedTableOuterRect.right() - 1),
1753 qBound(min: loadedTableOuterRect.y(), val: pos.y(), max: loadedTableOuterRect.bottom() - 1));
1754 QPointF clampedPosInView = q->mapFromItem(item: selectionPointerHandlerTarget(), point: clampedPos);
1755 clampedPosInView.rx() = qBound(min: 0., val: clampedPosInView.x(), max: viewportRect.width());
1756 clampedPosInView.ry() = qBound(min: 0., val: clampedPosInView.y(), max: viewportRect.height());
1757 clampedPos = q->mapToItem(item: selectionPointerHandlerTarget(), point: clampedPosInView);
1758
1759 return q->cellAtPosition(position: clampedPos, includeSpacing: true);
1760}
1761
1762void QQuickTableViewPrivate::updateSelection(const QRect &oldSelection, const QRect &newSelection)
1763{
1764 const QAbstractItemModel *qaim = selectionModel->model();
1765 const QRect oldRect = oldSelection.normalized();
1766 const QRect newRect = newSelection.normalized();
1767
1768 // Select cells inside the new selection rect
1769 {
1770 const QModelIndex startIndex = qaim->index(row: newRect.y(), column: newRect.x());
1771 const QModelIndex endIndex = qaim->index(row: newRect.y() + newRect.height(), column: newRect.x() + newRect.width());
1772 selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Select);
1773 }
1774
1775 // Unselect cells in the new minus old rects
1776 if (oldRect.x() < newRect.x()) {
1777 const QModelIndex startIndex = qaim->index(row: oldRect.y(), column: oldRect.x());
1778 const QModelIndex endIndex = qaim->index(row: oldRect.y() + oldRect.height(), column: newRect.x() - 1);
1779 selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Deselect);
1780 } else if (oldRect.x() + oldRect.width() > newRect.x() + newRect.width()) {
1781 const QModelIndex startIndex = qaim->index(row: oldRect.y(), column: newRect.x() + newRect.width() + 1);
1782 const QModelIndex endIndex = qaim->index(row: oldRect.y() + oldRect.height(), column: oldRect.x() + oldRect.width());
1783 selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Deselect);
1784 }
1785
1786 if (oldRect.y() < newRect.y()) {
1787 const QModelIndex startIndex = qaim->index(row: oldRect.y(), column: oldRect.x());
1788 const QModelIndex endIndex = qaim->index(row: newRect.y() - 1, column: oldRect.x() + oldRect.width());
1789 selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Deselect);
1790 } else if (oldRect.y() + oldRect.height() > newRect.y() + newRect.height()) {
1791 const QModelIndex startIndex = qaim->index(row: newRect.y() + newRect.height() + 1, column: oldRect.x());
1792 const QModelIndex endIndex = qaim->index(row: oldRect.y() + oldRect.height(), column: oldRect.x() + oldRect.width());
1793 selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Deselect);
1794 }
1795}
1796
1797void QQuickTableViewPrivate::clearSelection()
1798{
1799 selectionStartCell = QPoint(-1, -1);
1800 selectionEndCell = QPoint(-1, -1);
1801
1802 if (selectionModel)
1803 selectionModel->clearSelection();
1804}
1805
1806void QQuickTableViewPrivate::normalizeSelection()
1807{
1808 // Normalize the selection if necessary, so that the start cell is to the left
1809 // and above the end cell. This is typically done after a selection drag has
1810 // finished so that the start and end positions up in sync with the handles.
1811 // This will not cause any changes to the selection itself.
1812 if (selectionEndCell.x() < selectionStartCell.x())
1813 std::swap(a&: selectionStartCell.rx(), b&: selectionEndCell.rx());
1814 if (selectionEndCell.y() < selectionStartCell.y())
1815 std::swap(a&: selectionStartCell.ry(), b&: selectionEndCell.ry());
1816}
1817
1818QRectF QQuickTableViewPrivate::selectionRectangle() const
1819{
1820 Q_Q(const QQuickTableView);
1821
1822 QPoint topLeftCell = selectionStartCell;
1823 QPoint bottomRightCell = selectionEndCell;
1824 if (bottomRightCell.x() < topLeftCell.x())
1825 std::swap(a&: topLeftCell.rx(), b&: bottomRightCell.rx());
1826 if (selectionEndCell.y() < topLeftCell.y())
1827 std::swap(a&: topLeftCell.ry(), b&: bottomRightCell.ry());
1828
1829 const QPoint leftCell(topLeftCell.x(), topRow());
1830 const QPoint topCell(leftColumn(), topLeftCell.y());
1831 const QPoint rightCell(bottomRightCell.x(), topRow());
1832 const QPoint bottomCell(leftColumn(), bottomRightCell.y());
1833
1834 // If the corner cells of the selection are loaded, we can position the
1835 // selection rectangle at its exact location. Otherwise we extend it out
1836 // to the edges of the content item. This is not ideal, but the best we
1837 // can do while the location of the corner cells are unknown.
1838 // This will at least move the selection handles (and other overlay) out
1839 // of the viewport until the affected cells are eventually loaded.
1840 int left = 0;
1841 int top = 0;
1842 int right = 0;
1843 int bottom = 0;
1844
1845 if (loadedItems.contains(key: modelIndexAtCell(cell: leftCell)))
1846 left = loadedTableItem(cell: leftCell)->geometry().left();
1847 else if (leftCell.x() > rightColumn())
1848 left = q->contentWidth();
1849
1850 if (loadedItems.contains(key: modelIndexAtCell(cell: topCell)))
1851 top = loadedTableItem(cell: topCell)->geometry().top();
1852 else if (topCell.y() > bottomRow())
1853 top = q->contentHeight();
1854
1855 if (loadedItems.contains(key: modelIndexAtCell(cell: rightCell)))
1856 right = loadedTableItem(cell: rightCell)->geometry().right();
1857 else if (rightCell.x() > rightColumn())
1858 right = q->contentWidth();
1859
1860 if (loadedItems.contains(key: modelIndexAtCell(cell: bottomCell)))
1861 bottom = loadedTableItem(cell: bottomCell)->geometry().bottom();
1862 else if (bottomCell.y() > bottomRow())
1863 bottom = q->contentHeight();
1864
1865 return QRectF(left, top, right - left, bottom - top);
1866}
1867
1868QRect QQuickTableViewPrivate::selection() const
1869{
1870 const qreal w = selectionEndCell.x() - selectionStartCell.x();
1871 const qreal h = selectionEndCell.y() - selectionStartCell.y();
1872 return QRect(selectionStartCell.x(), selectionStartCell.y(), w, h);
1873}
1874
1875QSizeF QQuickTableViewPrivate::scrollTowardsSelectionPoint(const QPointF &pos, const QSizeF &step)
1876{
1877 Q_Q(QQuickTableView);
1878
1879 if (loadedItems.isEmpty())
1880 return QSizeF();
1881
1882 // Scroll the content item towards pos.
1883 // Return the distance in pixels from the edge of the viewport to pos.
1884 // The caller will typically use this information to throttle the scrolling speed.
1885 // If pos is already inside the viewport, or the viewport is scrolled all the way
1886 // to the end, we return 0.
1887 QSizeF dist(0, 0);
1888
1889 const bool outsideLeft = pos.x() < viewportRect.x();
1890 const bool outsideRight = pos.x() >= viewportRect.right() - 1;
1891 const bool outsideTop = pos.y() < viewportRect.y();
1892 const bool outsideBottom = pos.y() >= viewportRect.bottom() - 1;
1893
1894 if (outsideLeft) {
1895 const bool firstColumnLoaded = atTableEnd(edge: Qt::LeftEdge);
1896 const qreal remainingDist = viewportRect.left() - loadedTableOuterRect.left();
1897 if (remainingDist > 0 || !firstColumnLoaded) {
1898 qreal stepX = step.width();
1899 if (firstColumnLoaded)
1900 stepX = qMin(a: stepX, b: remainingDist);
1901 q->setContentX(q->contentX() - stepX);
1902 dist.setWidth(pos.x() - viewportRect.left() - 1);
1903 }
1904 } else if (outsideRight) {
1905 const bool lastColumnLoaded = atTableEnd(edge: Qt::RightEdge);
1906 const qreal remainingDist = loadedTableOuterRect.right() - viewportRect.right();
1907 if (remainingDist > 0 || !lastColumnLoaded) {
1908 qreal stepX = step.width();
1909 if (lastColumnLoaded)
1910 stepX = qMin(a: stepX, b: remainingDist);
1911 q->setContentX(q->contentX() + stepX);
1912 dist.setWidth(pos.x() - viewportRect.right() - 1);
1913 }
1914 }
1915
1916 if (outsideTop) {
1917 const bool firstRowLoaded = atTableEnd(edge: Qt::TopEdge);
1918 const qreal remainingDist = viewportRect.top() - loadedTableOuterRect.top();
1919 if (remainingDist > 0 || !firstRowLoaded) {
1920 qreal stepY = step.height();
1921 if (firstRowLoaded)
1922 stepY = qMin(a: stepY, b: remainingDist);
1923 q->setContentY(q->contentY() - stepY);
1924 dist.setHeight(pos.y() - viewportRect.top() - 1);
1925 }
1926 } else if (outsideBottom) {
1927 const bool lastRowLoaded = atTableEnd(edge: Qt::BottomEdge);
1928 const qreal remainingDist = loadedTableOuterRect.bottom() - viewportRect.bottom();
1929 if (remainingDist > 0 || !lastRowLoaded) {
1930 qreal stepY = step.height();
1931 if (lastRowLoaded)
1932 stepY = qMin(a: stepY, b: remainingDist);
1933 q->setContentY(q->contentY() + stepY);
1934 dist.setHeight(pos.y() - viewportRect.bottom() - 1);
1935 }
1936 }
1937
1938 return dist;
1939}
1940
1941QQuickTableViewAttached *QQuickTableViewPrivate::getAttachedObject(const QObject *object) const
1942{
1943 QObject *attachedObject = qmlAttachedPropertiesObject<QQuickTableView>(obj: object);
1944 return static_cast<QQuickTableViewAttached *>(attachedObject);
1945}
1946
1947int QQuickTableViewPrivate::modelIndexAtCell(const QPoint &cell) const
1948{
1949 // QQmlTableInstanceModel expects index to be in column-major
1950 // order. This means that if the view is transposed (with a flipped
1951 // width and height), we need to calculate it in row-major instead.
1952 if (isTransposed) {
1953 int availableColumns = tableSize.width();
1954 return (cell.y() * availableColumns) + cell.x();
1955 } else {
1956 int availableRows = tableSize.height();
1957 return (cell.x() * availableRows) + cell.y();
1958 }
1959}
1960
1961QPoint QQuickTableViewPrivate::cellAtModelIndex(int modelIndex) const
1962{
1963 // QQmlTableInstanceModel expects index to be in column-major
1964 // order. This means that if the view is transposed (with a flipped
1965 // width and height), we need to calculate it in row-major instead.
1966 if (isTransposed) {
1967 int availableColumns = tableSize.width();
1968 int row = int(modelIndex / availableColumns);
1969 int column = modelIndex % availableColumns;
1970 return QPoint(column, row);
1971 } else {
1972 int availableRows = tableSize.height();
1973 int column = int(modelIndex / availableRows);
1974 int row = modelIndex % availableRows;
1975 return QPoint(column, row);
1976 }
1977}
1978
1979int QQuickTableViewPrivate::modelIndexToCellIndex(const QModelIndex &modelIndex) const
1980{
1981 // Convert QModelIndex to cell index. A cell index is just an
1982 // integer representation of a cell instead of using a QPoint.
1983 const QPoint cell = q_func()->cellAtIndex(index: modelIndex);
1984 if (!cellIsValid(cell))
1985 return -1;
1986 return modelIndexAtCell(cell);
1987}
1988
1989int QQuickTableViewPrivate::edgeToArrayIndex(Qt::Edge edge) const
1990{
1991 return int(log2(x: float(edge)));
1992}
1993
1994void QQuickTableViewPrivate::clearEdgeSizeCache()
1995{
1996 cachedColumnWidth.startIndex = kEdgeIndexNotSet;
1997 cachedRowHeight.startIndex = kEdgeIndexNotSet;
1998
1999 for (Qt::Edge edge : allTableEdges)
2000 cachedNextVisibleEdgeIndex[edgeToArrayIndex(edge)].startIndex = kEdgeIndexNotSet;
2001}
2002
2003int QQuickTableViewPrivate::nextVisibleEdgeIndexAroundLoadedTable(Qt::Edge edge) const
2004{
2005 // Find the next column (or row) around the loaded table that is
2006 // visible, and should be loaded next if the content item moves.
2007 int startIndex = -1;
2008 switch (edge) {
2009 case Qt::LeftEdge: startIndex = leftColumn() - 1; break;
2010 case Qt::RightEdge: startIndex = rightColumn() + 1; break;
2011 case Qt::TopEdge: startIndex = topRow() - 1; break;
2012 case Qt::BottomEdge: startIndex = bottomRow() + 1; break;
2013 }
2014
2015 return nextVisibleEdgeIndex(edge, startIndex);
2016}
2017
2018int QQuickTableViewPrivate::nextVisibleEdgeIndex(Qt::Edge edge, int startIndex) const
2019{
2020 // First check if we have already searched for the first visible index
2021 // after the given startIndex recently, and if so, return the cached result.
2022 // The cached result is valid if startIndex is inside the range between the
2023 // startIndex and the first visible index found after it.
2024 auto &cachedResult = cachedNextVisibleEdgeIndex[edgeToArrayIndex(edge)];
2025 if (cachedResult.containsIndex(edge, index: startIndex))
2026 return cachedResult.endIndex;
2027
2028 // Search for the first column (or row) in the direction of edge that is
2029 // visible, starting from the given column (startIndex).
2030 int foundIndex = kEdgeIndexNotSet;
2031 int testIndex = startIndex;
2032
2033 switch (edge) {
2034 case Qt::LeftEdge: {
2035 forever {
2036 if (testIndex < 0) {
2037 foundIndex = kEdgeIndexAtEnd;
2038 break;
2039 }
2040
2041 if (!isColumnHidden(column: testIndex)) {
2042 foundIndex = testIndex;
2043 break;
2044 }
2045
2046 --testIndex;
2047 }
2048 break; }
2049 case Qt::RightEdge: {
2050 forever {
2051 if (testIndex > tableSize.width() - 1) {
2052 foundIndex = kEdgeIndexAtEnd;
2053 break;
2054 }
2055
2056 if (!isColumnHidden(column: testIndex)) {
2057 foundIndex = testIndex;
2058 break;
2059 }
2060
2061 ++testIndex;
2062 }
2063 break; }
2064 case Qt::TopEdge: {
2065 forever {
2066 if (testIndex < 0) {
2067 foundIndex = kEdgeIndexAtEnd;
2068 break;
2069 }
2070
2071 if (!isRowHidden(row: testIndex)) {
2072 foundIndex = testIndex;
2073 break;
2074 }
2075
2076 --testIndex;
2077 }
2078 break; }
2079 case Qt::BottomEdge: {
2080 forever {
2081 if (testIndex > tableSize.height() - 1) {
2082 foundIndex = kEdgeIndexAtEnd;
2083 break;
2084 }
2085
2086 if (!isRowHidden(row: testIndex)) {
2087 foundIndex = testIndex;
2088 break;
2089 }
2090
2091 ++testIndex;
2092 }
2093 break; }
2094 }
2095
2096 cachedResult.startIndex = startIndex;
2097 cachedResult.endIndex = foundIndex;
2098 return foundIndex;
2099}
2100
2101void QQuickTableViewPrivate::updateContentWidth()
2102{
2103 // Note that we actually never really know what the content size / size of the full table will
2104 // be. Even if e.g spacing changes, and we normally would assume that the size of the table
2105 // would increase accordingly, the model might also at some point have removed/hidden/resized
2106 // rows/columns outside the viewport. This would also affect the size, but since we don't load
2107 // rows or columns outside the viewport, this information is ignored. And even if we did, we
2108 // might also have been fast-flicked to a new location at some point, and started a new rebuild
2109 // there based on a new guesstimated top-left cell. So the calculated content size should always
2110 // be understood as a guesstimate, which sometimes can be really off (as a tradeoff for performance).
2111 // When this is not acceptable, the user can always set a custom content size explicitly.
2112 Q_Q(QQuickTableView);
2113
2114 if (syncHorizontally) {
2115 QBoolBlocker fixupGuard(inUpdateContentSize, true);
2116 q->QQuickFlickable::setContentWidth(syncView->contentWidth());
2117 return;
2118 }
2119
2120 if (explicitContentWidth.isValid()) {
2121 // Don't calculate contentWidth when it
2122 // was set explicitly by the application.
2123 return;
2124 }
2125
2126 if (loadedItems.isEmpty()) {
2127 QBoolBlocker fixupGuard(inUpdateContentSize, true);
2128 if (model && model->count() > 0 && tableModel && tableModel->delegate())
2129 q->QQuickFlickable::setContentWidth(kDefaultColumnWidth);
2130 else
2131 q->QQuickFlickable::setContentWidth(0);
2132 return;
2133 }
2134
2135 const int nextColumn = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::RightEdge);
2136 const int columnsRemaining = nextColumn == kEdgeIndexAtEnd ? 0 : tableSize.width() - nextColumn;
2137 const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
2138 const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
2139 const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
2140 const qreal estimatedWidth = loadedTableOuterRect.right() + estimatedRemainingWidth;
2141
2142 QBoolBlocker fixupGuard(inUpdateContentSize, true);
2143 q->QQuickFlickable::setContentWidth(estimatedWidth);
2144}
2145
2146void QQuickTableViewPrivate::updateContentHeight()
2147{
2148 Q_Q(QQuickTableView);
2149
2150 if (syncVertically) {
2151 QBoolBlocker fixupGuard(inUpdateContentSize, true);
2152 q->QQuickFlickable::setContentHeight(syncView->contentHeight());
2153 return;
2154 }
2155
2156 if (explicitContentHeight.isValid()) {
2157 // Don't calculate contentHeight when it
2158 // was set explicitly by the application.
2159 return;
2160 }
2161
2162 if (loadedItems.isEmpty()) {
2163 QBoolBlocker fixupGuard(inUpdateContentSize, true);
2164 if (model && model->count() > 0 && tableModel && tableModel->delegate())
2165 q->QQuickFlickable::setContentHeight(kDefaultRowHeight);
2166 else
2167 q->QQuickFlickable::setContentHeight(0);
2168 return;
2169 }
2170
2171 const int nextRow = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::BottomEdge);
2172 const int rowsRemaining = nextRow == kEdgeIndexAtEnd ? 0 : tableSize.height() - nextRow;
2173 const qreal remainingRowHeights = rowsRemaining * averageEdgeSize.height();
2174 const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
2175 const qreal estimatedRemainingHeight = remainingRowHeights + remainingSpacing;
2176 const qreal estimatedHeight = loadedTableOuterRect.bottom() + estimatedRemainingHeight;
2177
2178 QBoolBlocker fixupGuard(inUpdateContentSize, true);
2179 q->QQuickFlickable::setContentHeight(estimatedHeight);
2180}
2181
2182void QQuickTableViewPrivate::updateExtents()
2183{
2184 // When rows or columns outside the viewport are removed or added, or a rebuild
2185 // forces us to guesstimate a new top-left, the edges of the table might end up
2186 // out of sync with the edges of the content view. We detect this situation here, and
2187 // move the origin to ensure that there will never be gaps at the end of the table.
2188 // Normally we detect that the size of the whole table is not going to be equal to the
2189 // size of the content view already when we load the last row/column, and especially
2190 // before it's flicked completely inside the viewport. For those cases we simply adjust
2191 // the origin/endExtent, to give a smooth flicking experience.
2192 // But if flicking fast (e.g with a scrollbar), it can happen that the viewport ends up
2193 // outside the end of the table in just one viewport update. To avoid a "blink" in the
2194 // viewport when that happens, we "move" the loaded table into the viewport to cover it.
2195 Q_Q(QQuickTableView);
2196
2197 bool tableMovedHorizontally = false;
2198 bool tableMovedVertically = false;
2199
2200 const int nextLeftColumn = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::LeftEdge);
2201 const int nextRightColumn = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::RightEdge);
2202 const int nextTopRow = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::TopEdge);
2203 const int nextBottomRow = nextVisibleEdgeIndexAroundLoadedTable(edge: Qt::BottomEdge);
2204
2205 if (syncHorizontally) {
2206 const auto syncView_d = syncView->d_func();
2207 origin.rx() = syncView_d->origin.x();
2208 endExtent.rwidth() = syncView_d->endExtent.width();
2209 hData.markExtentsDirty();
2210 } else if (nextLeftColumn == kEdgeIndexAtEnd) {
2211 // There are no more columns to load on the left side of the table.
2212 // In that case, we ensure that the origin match the beginning of the table.
2213 if (loadedTableOuterRect.left() > viewportRect.left()) {
2214 // We have a blank area at the left end of the viewport. In that case we don't have time to
2215 // wait for the viewport to move (after changing origin), since that will take an extra
2216 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
2217 // us overshooting, we brute force the loaded table inside the already existing viewport.
2218 if (loadedTableOuterRect.left() > origin.x()) {
2219 const qreal diff = loadedTableOuterRect.left() - origin.x();
2220 loadedTableOuterRect.moveLeft(pos: loadedTableOuterRect.left() - diff);
2221 loadedTableInnerRect.moveLeft(pos: loadedTableInnerRect.left() - diff);
2222 tableMovedHorizontally = true;
2223 }
2224 }
2225 origin.rx() = loadedTableOuterRect.left();
2226 hData.markExtentsDirty();
2227 } else if (loadedTableOuterRect.left() <= origin.x() + cellSpacing.width()) {
2228 // The table rect is at the origin, or outside, but we still have more
2229 // visible columns to the left. So we try to guesstimate how much space
2230 // the rest of the columns will occupy, and move the origin accordingly.
2231 const int columnsRemaining = nextLeftColumn + 1;
2232 const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
2233 const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
2234 const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
2235 origin.rx() = loadedTableOuterRect.left() - estimatedRemainingWidth;
2236 hData.markExtentsDirty();
2237 } else if (nextRightColumn == kEdgeIndexAtEnd) {
2238 // There are no more columns to load on the right side of the table.
2239 // In that case, we ensure that the end of the content view match the end of the table.
2240 if (loadedTableOuterRect.right() < viewportRect.right()) {
2241 // We have a blank area at the right end of the viewport. In that case we don't have time to
2242 // wait for the viewport to move (after changing endExtent), since that will take an extra
2243 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
2244 // us overshooting, we brute force the loaded table inside the already existing viewport.
2245 const qreal w = qMin(a: viewportRect.right(), b: q->contentWidth() + endExtent.width());
2246 if (loadedTableOuterRect.right() < w) {
2247 const qreal diff = loadedTableOuterRect.right() - w;
2248 loadedTableOuterRect.moveRight(pos: loadedTableOuterRect.right() - diff);
2249 loadedTableInnerRect.moveRight(pos: loadedTableInnerRect.right() - diff);
2250 tableMovedHorizontally = true;
2251 }
2252 }
2253 endExtent.rwidth() = loadedTableOuterRect.right() - q->contentWidth();
2254 hData.markExtentsDirty();
2255 } else if (loadedTableOuterRect.right() >= q->contentWidth() + endExtent.width() - cellSpacing.width()) {
2256 // The right-most column is outside the end of the content view, and we
2257 // still have more visible columns in the model. This can happen if the application
2258 // has set a fixed content width.
2259 const int columnsRemaining = tableSize.width() - nextRightColumn;
2260 const qreal remainingColumnWidths = columnsRemaining * averageEdgeSize.width();
2261 const qreal remainingSpacing = columnsRemaining * cellSpacing.width();
2262 const qreal estimatedRemainingWidth = remainingColumnWidths + remainingSpacing;
2263 const qreal pixelsOutsideContentWidth = loadedTableOuterRect.right() - q->contentWidth();
2264 endExtent.rwidth() = pixelsOutsideContentWidth + estimatedRemainingWidth;
2265 hData.markExtentsDirty();
2266 }
2267
2268 if (syncVertically) {
2269 const auto syncView_d = syncView->d_func();
2270 origin.ry() = syncView_d->origin.y();
2271 endExtent.rheight() = syncView_d->endExtent.height();
2272 vData.markExtentsDirty();
2273 } else if (nextTopRow == kEdgeIndexAtEnd) {
2274 // There are no more rows to load on the top side of the table.
2275 // In that case, we ensure that the origin match the beginning of the table.
2276 if (loadedTableOuterRect.top() > viewportRect.top()) {
2277 // We have a blank area at the top of the viewport. In that case we don't have time to
2278 // wait for the viewport to move (after changing origin), since that will take an extra
2279 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
2280 // us overshooting, we brute force the loaded table inside the already existing viewport.
2281 if (loadedTableOuterRect.top() > origin.y()) {
2282 const qreal diff = loadedTableOuterRect.top() - origin.y();
2283 loadedTableOuterRect.moveTop(pos: loadedTableOuterRect.top() - diff);
2284 loadedTableInnerRect.moveTop(pos: loadedTableInnerRect.top() - diff);
2285 tableMovedVertically = true;
2286 }
2287 }
2288 origin.ry() = loadedTableOuterRect.top();
2289 vData.markExtentsDirty();
2290 } else if (loadedTableOuterRect.top() <= origin.y() + cellSpacing.height()) {
2291 // The table rect is at the origin, or outside, but we still have more
2292 // visible rows at the top. So we try to guesstimate how much space
2293 // the rest of the rows will occupy, and move the origin accordingly.
2294 const int rowsRemaining = nextTopRow + 1;
2295 const qreal remainingRowHeights = rowsRemaining * averageEdgeSize.height();
2296 const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
2297 const qreal estimatedRemainingHeight = remainingRowHeights + remainingSpacing;
2298 origin.ry() = loadedTableOuterRect.top() - estimatedRemainingHeight;
2299 vData.markExtentsDirty();
2300 } else if (nextBottomRow == kEdgeIndexAtEnd) {
2301 // There are no more rows to load on the bottom side of the table.
2302 // In that case, we ensure that the end of the content view match the end of the table.
2303 if (loadedTableOuterRect.bottom() < viewportRect.bottom()) {
2304 // We have a blank area at the bottom of the viewport. In that case we don't have time to
2305 // wait for the viewport to move (after changing endExtent), since that will take an extra
2306 // update cycle, which will be visible as a blink. Instead, unless the blank spot is just
2307 // us overshooting, we brute force the loaded table inside the already existing viewport.
2308 const qreal h = qMin(a: viewportRect.bottom(), b: q->contentHeight() + endExtent.height());
2309 if (loadedTableOuterRect.bottom() < h) {
2310 const qreal diff = loadedTableOuterRect.bottom() - h;
2311 loadedTableOuterRect.moveBottom(pos: loadedTableOuterRect.bottom() - diff);
2312 loadedTableInnerRect.moveBottom(pos: loadedTableInnerRect.bottom() - diff);
2313 tableMovedVertically = true;
2314 }
2315 }
2316 endExtent.rheight() = loadedTableOuterRect.bottom() - q->contentHeight();
2317 vData.markExtentsDirty();
2318 } else if (loadedTableOuterRect.bottom() >= q->contentHeight() + endExtent.height() - cellSpacing.height()) {
2319 // The bottom-most row is outside the end of the content view, and we
2320 // still have more visible rows in the model. This can happen if the application
2321 // has set a fixed content height.
2322 const int rowsRemaining = tableSize.height() - nextBottomRow;
2323 const qreal remainingRowHeigts = rowsRemaining * averageEdgeSize.height();
2324 const qreal remainingSpacing = rowsRemaining * cellSpacing.height();
2325 const qreal estimatedRemainingHeight = remainingRowHeigts + remainingSpacing;
2326 const qreal pixelsOutsideContentHeight = loadedTableOuterRect.bottom() - q->contentHeight();
2327 endExtent.rheight() = pixelsOutsideContentHeight + estimatedRemainingHeight;
2328 vData.markExtentsDirty();
2329 }
2330
2331 if (tableMovedHorizontally || tableMovedVertically) {
2332 qCDebug(lcTableViewDelegateLifecycle) << "move table to" << loadedTableOuterRect;
2333
2334 // relayoutTableItems() will take care of moving the existing
2335 // delegate items into the new loadedTableOuterRect.
2336 relayoutTableItems();
2337
2338 // Inform the sync children that they need to rebuild to stay in sync
2339 for (auto syncChild : std::as_const(t&: syncChildren)) {
2340 auto syncChild_d = syncChild->d_func();
2341 syncChild_d->scheduledRebuildOptions |= RebuildOption::ViewportOnly;
2342 if (tableMovedHorizontally)
2343 syncChild_d->scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn;
2344 if (tableMovedVertically)
2345 syncChild_d->scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow;
2346 }
2347 }
2348
2349 if (hData.minExtentDirty || vData.minExtentDirty) {
2350 qCDebug(lcTableViewDelegateLifecycle) << "move origin and endExtent to:" << origin << endExtent;
2351 // updateBeginningEnd() will let the new extents take effect. This will also change the
2352 // visualArea of the flickable, which again will cause any attached scrollbars to adjust
2353 // the position of the handle. Note the latter will cause the viewport to move once more.
2354 updateBeginningEnd();
2355 }
2356}
2357
2358void QQuickTableViewPrivate::updateAverageColumnWidth()
2359{
2360 if (explicitContentWidth.isValid()) {
2361 const qreal accColumnSpacing = (tableSize.width() - 1) * cellSpacing.width();
2362 averageEdgeSize.setWidth((explicitContentWidth - accColumnSpacing) / tableSize.width());
2363 } else {
2364 const qreal accColumnSpacing = (loadedColumns.count() - 1) * cellSpacing.width();
2365 averageEdgeSize.setWidth((loadedTableOuterRect.width() - accColumnSpacing) / loadedColumns.count());
2366 }
2367}
2368
2369void QQuickTableViewPrivate::updateAverageRowHeight()
2370{
2371 if (explicitContentHeight.isValid()) {
2372 const qreal accRowSpacing = (tableSize.height() - 1) * cellSpacing.height();
2373 averageEdgeSize.setHeight((explicitContentHeight - accRowSpacing) / tableSize.height());
2374 } else {
2375 const qreal accRowSpacing = (loadedRows.count() - 1) * cellSpacing.height();
2376 averageEdgeSize.setHeight((loadedTableOuterRect.height() - accRowSpacing) / loadedRows.count());
2377 }
2378}
2379
2380void QQuickTableViewPrivate::syncLoadedTableRectFromLoadedTable()
2381{
2382 const QPoint topLeft = QPoint(leftColumn(), topRow());
2383 const QPoint bottomRight = QPoint(rightColumn(), bottomRow());
2384 QRectF topLeftRect = loadedTableItem(cell: topLeft)->geometry();
2385 QRectF bottomRightRect = loadedTableItem(cell: bottomRight)->geometry();
2386 loadedTableOuterRect = QRectF(topLeftRect.topLeft(), bottomRightRect.bottomRight());
2387 loadedTableInnerRect = QRectF(topLeftRect.bottomRight(), bottomRightRect.topLeft());
2388}
2389
2390void QQuickTableViewPrivate::shiftLoadedTableRect(const QPointF newPosition)
2391{
2392 // Move the tracked table rects to the new position. For this to
2393 // take visual effect (move the delegate items to be inside the table
2394 // rect), it needs to be followed by a relayoutTableItems().
2395 // Also note that the position of the viewport needs to be adjusted
2396 // separately for it to overlap the loaded table.
2397 const QPointF innerDiff = loadedTableOuterRect.topLeft() - loadedTableInnerRect.topLeft();
2398 loadedTableOuterRect.moveTopLeft(p: newPosition);
2399 loadedTableInnerRect.moveTopLeft(p: newPosition + innerDiff);
2400}
2401
2402QQuickTableViewPrivate::RebuildOptions QQuickTableViewPrivate::checkForVisibilityChanges()
2403{
2404 // This function will check if there are any visibility changes among
2405 // the _already loaded_ rows and columns. Note that there can be rows
2406 // and columns to the bottom or right that was not loaded, but should
2407 // now become visible (in case there is free space around the table).
2408 if (loadedItems.isEmpty()) {
2409 // Report no changes
2410 return RebuildOption::None;
2411 }
2412
2413 RebuildOptions rebuildOptions = RebuildOption::None;
2414
2415 if (loadedTableOuterRect.x() == origin.x() && leftColumn() != 0) {
2416 // Since the left column is at the origin of the viewport, but still not the first
2417 // column in the model, we need to calculate a new left column since there might be
2418 // columns in front of it that used to be hidden, but should now be visible (QTBUG-93264).
2419 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
2420 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftColumn);
2421 } else {
2422 // Go through all loaded columns from first to last, find the columns that used
2423 // to be hidden and not loaded, and check if they should become visible
2424 // (and vice versa). If there is a change, we need to rebuild.
2425 for (int column = leftColumn(); column <= rightColumn(); ++column) {
2426 const bool wasVisibleFromBefore = loadedColumns.contains(v: column);
2427 const bool isVisibleNow = !qFuzzyIsNull(d: getColumnWidth(column));
2428 if (wasVisibleFromBefore == isVisibleNow)
2429 continue;
2430
2431 // A column changed visibility. This means that it should
2432 // either be loaded or unloaded. So we need a rebuild.
2433 qCDebug(lcTableViewDelegateLifecycle) << "Column" << column << "changed visibility to" << isVisibleNow;
2434 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
2435 if (column == leftColumn()) {
2436 // The first loaded column should now be hidden. This means that we
2437 // need to calculate which column should now be first instead.
2438 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftColumn);
2439 }
2440 break;
2441 }
2442 }
2443
2444 if (loadedTableOuterRect.y() == origin.y() && topRow() != 0) {
2445 // Since the top row is at the origin of the viewport, but still not the first
2446 // row in the model, we need to calculate a new top row since there might be
2447 // rows in front of it that used to be hidden, but should now be visible (QTBUG-93264).
2448 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
2449 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftRow);
2450 } else {
2451 // Go through all loaded rows from first to last, find the rows that used
2452 // to be hidden and not loaded, and check if they should become visible
2453 // (and vice versa). If there is a change, we need to rebuild.
2454 for (int row = topRow(); row <= bottomRow(); ++row) {
2455 const bool wasVisibleFromBefore = loadedRows.contains(v: row);
2456 const bool isVisibleNow = !qFuzzyIsNull(d: getRowHeight(row));
2457 if (wasVisibleFromBefore == isVisibleNow)
2458 continue;
2459
2460 // A row changed visibility. This means that it should
2461 // either be loaded or unloaded. So we need a rebuild.
2462 qCDebug(lcTableViewDelegateLifecycle) << "Row" << row << "changed visibility to" << isVisibleNow;
2463 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
2464 if (row == topRow())
2465 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftRow);
2466 break;
2467 }
2468 }
2469
2470 return rebuildOptions;
2471}
2472
2473void QQuickTableViewPrivate::forceLayout(bool immediate)
2474{
2475 clearEdgeSizeCache();
2476 RebuildOptions rebuildOptions = RebuildOption::None;
2477
2478 const QSize actualTableSize = calculateTableSize();
2479 if (tableSize != actualTableSize) {
2480 // This can happen if the app is calling forceLayout while
2481 // the model is updated, but before we're notified about it.
2482 rebuildOptions = RebuildOption::All;
2483 } else {
2484 // Resizing a column (or row) can result in the table going from being
2485 // e.g completely inside the viewport to go outside. And in the latter
2486 // case, the user needs to be able to scroll the viewport, also if
2487 // flags such as Flickable.StopAtBounds is in use. So we need to
2488 // update contentWidth/Height to support that case.
2489 rebuildOptions = RebuildOption::LayoutOnly
2490 | RebuildOption::CalculateNewContentWidth
2491 | RebuildOption::CalculateNewContentHeight
2492 | checkForVisibilityChanges();
2493 }
2494
2495 scheduleRebuildTable(options: rebuildOptions);
2496
2497 if (immediate) {
2498 auto rootView = rootSyncView();
2499 const bool updated = rootView->d_func()->updateTableRecursive();
2500 if (!updated) {
2501 qWarning() << "TableView::forceLayout(): Cannot do an immediate re-layout during an ongoing layout!";
2502 rootView->polish();
2503 }
2504 }
2505}
2506
2507void QQuickTableViewPrivate::syncLoadedTableFromLoadRequest()
2508{
2509 if (loadRequest.edge() == Qt::Edge(0)) {
2510 // No edge means we're loading the top-left item
2511 loadedColumns.insert(v: loadRequest.column());
2512 loadedRows.insert(v: loadRequest.row());
2513 return;
2514 }
2515
2516 switch (loadRequest.edge()) {
2517 case Qt::LeftEdge:
2518 case Qt::RightEdge:
2519 loadedColumns.insert(v: loadRequest.column());
2520 break;
2521 case Qt::TopEdge:
2522 case Qt::BottomEdge:
2523 loadedRows.insert(v: loadRequest.row());
2524 break;
2525 }
2526}
2527
2528FxTableItem *QQuickTableViewPrivate::loadedTableItem(const QPoint &cell) const
2529{
2530 const int modelIndex = modelIndexAtCell(cell);
2531 Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell);
2532 return loadedItems.value(key: modelIndex);
2533}
2534
2535FxTableItem *QQuickTableViewPrivate::createFxTableItem(const QPoint &cell, QQmlIncubator::IncubationMode incubationMode)
2536{
2537 Q_Q(QQuickTableView);
2538
2539 bool ownItem = false;
2540 int modelIndex = modelIndexAtCell(cell);
2541
2542 QObject* object = model->object(index: modelIndex, incubationMode);
2543 if (!object) {
2544 if (model->incubationStatus(index: modelIndex) == QQmlIncubator::Loading) {
2545 // Item is incubating. Return nullptr for now, and let the table call this
2546 // function again once we get a callback to itemCreatedCallback().
2547 return nullptr;
2548 }
2549
2550 qWarning() << "TableView: failed loading index:" << modelIndex;
2551 object = new QQuickItem();
2552 ownItem = true;
2553 }
2554
2555 QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
2556 if (!item) {
2557 // The model could not provide an QQuickItem for the
2558 // given index, so we create a placeholder instead.
2559 qWarning() << "TableView: delegate is not an item:" << modelIndex;
2560 model->release(object);
2561 item = new QQuickItem();
2562 ownItem = true;
2563 } else {
2564 QQuickAnchors *anchors = QQuickItemPrivate::get(item)->_anchors;
2565 if (anchors && anchors->activeDirections())
2566 qmlWarning(me: item) << "TableView: detected anchors on delegate with index: " << modelIndex
2567 << ". Use implicitWidth and implicitHeight instead.";
2568 }
2569
2570 if (ownItem) {
2571 // Parent item is normally set early on from initItemCallback (to
2572 // allow bindings to the parent property). But if we created the item
2573 // within this function, we need to set it explicit.
2574 item->setImplicitWidth(kDefaultColumnWidth);
2575 item->setImplicitHeight(kDefaultRowHeight);
2576 item->setParentItem(q->contentItem());
2577 }
2578 Q_TABLEVIEW_ASSERT(item->parentItem() == q->contentItem(), item->parentItem());
2579
2580 FxTableItem *fxTableItem = new FxTableItem(item, q, ownItem);
2581 fxTableItem->setVisible(false);
2582 fxTableItem->cell = cell;
2583 fxTableItem->index = modelIndex;
2584 return fxTableItem;
2585}
2586
2587FxTableItem *QQuickTableViewPrivate::loadFxTableItem(const QPoint &cell, QQmlIncubator::IncubationMode incubationMode)
2588{
2589#ifdef QT_DEBUG
2590 // Since TableView needs to work flawlessly when e.g incubating inside an async
2591 // loader, being able to override all loading to async while debugging can be helpful.
2592 static const bool forcedAsync = forcedIncubationMode == QLatin1String("async");
2593 if (forcedAsync)
2594 incubationMode = QQmlIncubator::Asynchronous;
2595#endif
2596
2597 // Note that even if incubation mode is asynchronous, the item might
2598 // be ready immediately since the model has a cache of items.
2599 QBoolBlocker guard(blockItemCreatedCallback);
2600 auto item = createFxTableItem(cell, incubationMode);
2601 qCDebug(lcTableViewDelegateLifecycle) << cell << "ready?" << bool(item);
2602 return item;
2603}
2604
2605void QQuickTableViewPrivate::releaseLoadedItems(QQmlTableInstanceModel::ReusableFlag reusableFlag) {
2606 // Make a copy and clear the list of items first to avoid destroyed
2607 // items being accessed during the loop (QTBUG-61294)
2608 auto const tmpList = loadedItems;
2609 loadedItems.clear();
2610 for (FxTableItem *item : tmpList)
2611 releaseItem(fxTableItem: item, reusableFlag);
2612}
2613
2614void QQuickTableViewPrivate::releaseItem(FxTableItem *fxTableItem, QQmlTableInstanceModel::ReusableFlag reusableFlag)
2615{
2616 Q_Q(QQuickTableView);
2617 // Note that fxTableItem->item might already have been destroyed, in case
2618 // the item is owned by the QML context rather than the model (e.g ObjectModel etc).
2619 auto item = fxTableItem->item;
2620
2621 if (fxTableItem->ownItem) {
2622 Q_TABLEVIEW_ASSERT(item, fxTableItem->index);
2623 delete item;
2624 } else if (item) {
2625 auto releaseFlag = model->release(object: item, reusableFlag);
2626 if (releaseFlag == QQmlInstanceModel::Pooled) {
2627 fxTableItem->setVisible(false);
2628
2629 // If the item (or a descendant) has focus, remove it, so
2630 // that the item doesn't enter with focus when it's reused.
2631 if (QQuickWindow *window = item->window()) {
2632 const auto focusItem = qobject_cast<QQuickItem *>(o: window->focusObject());
2633 if (focusItem) {
2634 const bool hasFocus = item == focusItem || item->isAncestorOf(child: focusItem);
2635 if (hasFocus) {
2636 const auto focusChild = QQuickItemPrivate::get(item: q)->subFocusItem;
2637 deliveryAgentPrivate()->clearFocusInScope(scope: q, item: focusChild, reason: Qt::OtherFocusReason);
2638 }
2639 }
2640 }
2641 }
2642 }
2643
2644 delete fxTableItem;
2645}
2646
2647void QQuickTableViewPrivate::unloadItem(const QPoint &cell)
2648{
2649 const int modelIndex = modelIndexAtCell(cell);
2650 Q_TABLEVIEW_ASSERT(loadedItems.contains(modelIndex), modelIndex << cell);
2651 releaseItem(fxTableItem: loadedItems.take(key: modelIndex), reusableFlag);
2652}
2653
2654bool QQuickTableViewPrivate::canLoadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const
2655{
2656 switch (tableEdge) {
2657 case Qt::LeftEdge:
2658 return loadedTableOuterRect.left() > fillRect.left() + cellSpacing.width();
2659 case Qt::RightEdge:
2660 return loadedTableOuterRect.right() < fillRect.right() - cellSpacing.width();
2661 case Qt::TopEdge:
2662 return loadedTableOuterRect.top() > fillRect.top() + cellSpacing.height();
2663 case Qt::BottomEdge:
2664 return loadedTableOuterRect.bottom() < fillRect.bottom() - cellSpacing.height();
2665 }
2666
2667 return false;
2668}
2669
2670bool QQuickTableViewPrivate::canUnloadTableEdge(Qt::Edge tableEdge, const QRectF fillRect) const
2671{
2672 // Note: if there is only one row or column left, we cannot unload, since
2673 // they are needed as anchor point for further layouting. We also skip
2674 // unloading in the direction we're currently scrolling.
2675
2676 switch (tableEdge) {
2677 case Qt::LeftEdge:
2678 if (loadedColumns.count() <= 1)
2679 return false;
2680 if (positionXAnimation.isRunning()) {
2681 const qreal to = positionXAnimation.to().toFloat();
2682 if (to < viewportRect.x())
2683 return false;
2684 }
2685 return loadedTableInnerRect.left() <= fillRect.left();
2686 case Qt::RightEdge:
2687 if (loadedColumns.count() <= 1)
2688 return false;
2689 if (positionXAnimation.isRunning()) {
2690 const qreal to = positionXAnimation.to().toFloat();
2691 if (to > viewportRect.x())
2692 return false;
2693 }
2694 return loadedTableInnerRect.right() >= fillRect.right();
2695 case Qt::TopEdge:
2696 if (loadedRows.count() <= 1)
2697 return false;
2698 if (positionYAnimation.isRunning()) {
2699 const qreal to = positionYAnimation.to().toFloat();
2700 if (to < viewportRect.y())
2701 return false;
2702 }
2703 return loadedTableInnerRect.top() <= fillRect.top();
2704 case Qt::BottomEdge:
2705 if (loadedRows.count() <= 1)
2706 return false;
2707 if (positionYAnimation.isRunning()) {
2708 const qreal to = positionYAnimation.to().toFloat();
2709 if (to > viewportRect.y())
2710 return false;
2711 }
2712 return loadedTableInnerRect.bottom() >= fillRect.bottom();
2713 }
2714 Q_TABLEVIEW_UNREACHABLE(tableEdge);
2715 return false;
2716}
2717
2718Qt::Edge QQuickTableViewPrivate::nextEdgeToLoad(const QRectF rect)
2719{
2720 for (Qt::Edge edge : allTableEdges) {
2721 if (!canLoadTableEdge(tableEdge: edge, fillRect: rect))
2722 continue;
2723 const int nextIndex = nextVisibleEdgeIndexAroundLoadedTable(edge);
2724 if (nextIndex == kEdgeIndexAtEnd)
2725 continue;
2726 return edge;
2727 }
2728
2729 return Qt::Edge(0);
2730}
2731
2732Qt::Edge QQuickTableViewPrivate::nextEdgeToUnload(const QRectF rect)
2733{
2734 for (Qt::Edge edge : allTableEdges) {
2735 if (canUnloadTableEdge(tableEdge: edge, fillRect: rect))
2736 return edge;
2737 }
2738 return Qt::Edge(0);
2739}
2740
2741qreal QQuickTableViewPrivate::cellWidth(const QPoint& cell) const
2742{
2743 // Using an items width directly is not an option, since we change
2744 // it during layout (which would also cause problems when recycling items).
2745 auto const cellItem = loadedTableItem(cell)->item;
2746 return cellItem->implicitWidth();
2747}
2748
2749qreal QQuickTableViewPrivate::cellHeight(const QPoint& cell) const
2750{
2751 // Using an items height directly is not an option, since we change
2752 // it during layout (which would also cause problems when recycling items).
2753 auto const cellItem = loadedTableItem(cell)->item;
2754 return cellItem->implicitHeight();
2755}
2756
2757qreal QQuickTableViewPrivate::sizeHintForColumn(int column) const
2758{
2759 // Find the widest cell in the column, and return its width
2760 qreal columnWidth = 0;
2761 for (const int row : loadedRows)
2762 columnWidth = qMax(a: columnWidth, b: cellWidth(cell: QPoint(column, row)));
2763
2764 return columnWidth;
2765}
2766
2767qreal QQuickTableViewPrivate::sizeHintForRow(int row) const
2768{
2769 // Find the highest cell in the row, and return its height
2770 qreal rowHeight = 0;
2771 for (const int column : loadedColumns)
2772 rowHeight = qMax(a: rowHeight, b: cellHeight(cell: QPoint(column, row)));
2773 return rowHeight;
2774}
2775
2776void QQuickTableViewPrivate::updateTableSize()
2777{
2778 // tableSize is the same as row and column count, and will always
2779 // be the same as the number of rows and columns in the model.
2780 Q_Q(QQuickTableView);
2781
2782 const QSize prevTableSize = tableSize;
2783 tableSize = calculateTableSize();
2784
2785 if (prevTableSize.width() != tableSize.width())
2786 emit q->columnsChanged();
2787 if (prevTableSize.height() != tableSize.height())
2788 emit q->rowsChanged();
2789}
2790
2791QSize QQuickTableViewPrivate::calculateTableSize()
2792{
2793 QSize size(0, 0);
2794 if (tableModel)
2795 size = QSize(tableModel->columns(), tableModel->rows());
2796 else if (model)
2797 size = QSize(1, model->count());
2798
2799 return isTransposed ? size.transposed() : size;
2800}
2801
2802qreal QQuickTableViewPrivate::getColumnLayoutWidth(int column)
2803{
2804 // Return the column width specified by the application, or go
2805 // through the loaded items and calculate it as a fallback. For
2806 // layouting, the width can never be zero (or negative), as this
2807 // can lead us to be stuck in an infinite loop trying to load and
2808 // fill out the empty viewport space with empty columns.
2809 const qreal explicitColumnWidth = getColumnWidth(column);
2810 if (explicitColumnWidth >= 0)
2811 return explicitColumnWidth;
2812
2813 if (syncHorizontally) {
2814 if (syncView->d_func()->loadedColumns.contains(v: column))
2815 return syncView->d_func()->getColumnLayoutWidth(column);
2816 }
2817
2818 // Iterate over the currently visible items in the column. The downside
2819 // of doing that, is that the column width will then only be based on the implicit
2820 // width of the currently loaded items (which can be different depending on which
2821 // row you're at when the column is flicked in). The upshot is that you don't have to
2822 // bother setting columnWidthProvider for small tables, or if the implicit width doesn't vary.
2823 qreal columnWidth = sizeHintForColumn(column);
2824
2825 if (qIsNaN(d: columnWidth) || columnWidth <= 0) {
2826 if (!layoutWarningIssued) {
2827 layoutWarningIssued = true;
2828 qmlWarning(me: q_func()) << "the delegate's implicitWidth needs to be greater than zero";
2829 }
2830 columnWidth = kDefaultColumnWidth;
2831 }
2832
2833 return columnWidth;
2834}
2835
2836qreal QQuickTableViewPrivate::getEffectiveRowY(int row) const
2837{
2838 // Return y pos of row after layout
2839 Q_TABLEVIEW_ASSERT(loadedRows.contains(row), row);
2840 return loadedTableItem(cell: QPoint(leftColumn(), row))->geometry().y();
2841}
2842
2843qreal QQuickTableViewPrivate::getEffectiveRowHeight(int row) const
2844{
2845 // Return row height after layout
2846 Q_TABLEVIEW_ASSERT(loadedRows.contains(row), row);
2847 return loadedTableItem(cell: QPoint(leftColumn(), row))->geometry().height();
2848}
2849
2850qreal QQuickTableViewPrivate::getEffectiveColumnX(int column) const
2851{
2852 // Return x pos of column after layout
2853 Q_TABLEVIEW_ASSERT(loadedColumns.contains(column), column);
2854 return loadedTableItem(cell: QPoint(column, topRow()))->geometry().x();
2855}
2856
2857qreal QQuickTableViewPrivate::getEffectiveColumnWidth(int column) const
2858{
2859 // Return column width after layout
2860 Q_TABLEVIEW_ASSERT(loadedColumns.contains(column), column);
2861 return loadedTableItem(cell: QPoint(column, topRow()))->geometry().width();
2862}
2863
2864qreal QQuickTableViewPrivate::getRowLayoutHeight(int row)
2865{
2866 // Return the row height specified by the application, or go
2867 // through the loaded items and calculate it as a fallback. For
2868 // layouting, the height can never be zero (or negative), as this
2869 // can lead us to be stuck in an infinite loop trying to load and
2870 // fill out the empty viewport space with empty rows.
2871 const qreal explicitRowHeight = getRowHeight(row);
2872 if (explicitRowHeight >= 0)
2873 return explicitRowHeight;
2874
2875 if (syncVertically) {
2876 if (syncView->d_func()->loadedRows.contains(v: row))
2877 return syncView->d_func()->getRowLayoutHeight(row);
2878 }
2879
2880 // Iterate over the currently visible items in the row. The downside
2881 // of doing that, is that the row height will then only be based on the implicit
2882 // height of the currently loaded items (which can be different depending on which
2883 // column you're at when the row is flicked in). The upshot is that you don't have to
2884 // bother setting rowHeightProvider for small tables, or if the implicit height doesn't vary.
2885 qreal rowHeight = sizeHintForRow(row);
2886
2887 if (qIsNaN(d: rowHeight) || rowHeight <= 0) {
2888 if (!layoutWarningIssued) {
2889 layoutWarningIssued = true;
2890 qmlWarning(me: q_func()) << "the delegate's implicitHeight needs to be greater than zero";
2891 }
2892 rowHeight = kDefaultRowHeight;
2893 }
2894
2895 return rowHeight;
2896}
2897
2898qreal QQuickTableViewPrivate::getColumnWidth(int column) const
2899{
2900 // Return the width of the given column, if explicitly set. Return 0 if the column
2901 // is hidden, and -1 if the width is not set (which means that the width should
2902 // instead be calculated from the implicit size of the delegate items. This function
2903 // can be overridden by e.g HeaderView to provide the column widths by other means.
2904 Q_Q(const QQuickTableView);
2905
2906 const int noExplicitColumnWidth = -1;
2907
2908 if (cachedColumnWidth.startIndex == column)
2909 return cachedColumnWidth.size;
2910
2911 if (syncHorizontally)
2912 return syncView->d_func()->getColumnWidth(column);
2913
2914 if (columnWidthProvider.isUndefined()) {
2915 // We only respect explicit column widths when no columnWidthProvider
2916 // is set. Otherwise it's the responsibility of the provider to e.g
2917 // call explicitColumnWidth() (and implicitColumnWidth()), if needed.
2918 qreal explicitColumnWidth = q->explicitColumnWidth(column);
2919 if (explicitColumnWidth >= 0)
2920 return explicitColumnWidth;
2921 return noExplicitColumnWidth;
2922 }
2923
2924 qreal columnWidth = noExplicitColumnWidth;
2925
2926 if (columnWidthProvider.isCallable()) {
2927 auto const columnAsArgument = QJSValueList() << QJSValue(column);
2928 columnWidth = columnWidthProvider.call(args: columnAsArgument).toNumber();
2929 if (qIsNaN(d: columnWidth) || columnWidth < 0)
2930 columnWidth = noExplicitColumnWidth;
2931 } else {
2932 if (!layoutWarningIssued) {
2933 layoutWarningIssued = true;
2934 qmlWarning(me: q_func()) << "columnWidthProvider doesn't contain a function";
2935 }
2936 columnWidth = noExplicitColumnWidth;
2937 }
2938
2939 cachedColumnWidth.startIndex = column;
2940 cachedColumnWidth.size = columnWidth;
2941 return columnWidth;
2942}
2943
2944qreal QQuickTableViewPrivate::getRowHeight(int row) const
2945{
2946 // Return the height of the given row, if explicitly set. Return 0 if the row
2947 // is hidden, and -1 if the height is not set (which means that the height should
2948 // instead be calculated from the implicit size of the delegate items. This function
2949 // can be overridden by e.g HeaderView to provide the row heights by other means.
2950 Q_Q(const QQuickTableView);
2951
2952 const int noExplicitRowHeight = -1;
2953
2954 if (cachedRowHeight.startIndex == row)
2955 return cachedRowHeight.size;
2956
2957 if (syncVertically)
2958 return syncView->d_func()->getRowHeight(row);
2959
2960 if (rowHeightProvider.isUndefined()) {
2961 // We only resepect explicit row heights when no rowHeightProvider
2962 // is set. Otherwise it's the responsibility of the provider to e.g
2963 // call explicitRowHeight() (and implicitRowHeight()), if needed.
2964 qreal explicitRowHeight = q->explicitRowHeight(row);
2965 if (explicitRowHeight >= 0)
2966 return explicitRowHeight;
2967 return noExplicitRowHeight;
2968 }
2969
2970 qreal rowHeight = noExplicitRowHeight;
2971
2972 if (rowHeightProvider.isCallable()) {
2973 auto const rowAsArgument = QJSValueList() << QJSValue(row);
2974 rowHeight = rowHeightProvider.call(args: rowAsArgument).toNumber();
2975 if (qIsNaN(d: rowHeight) || rowHeight < 0)
2976 rowHeight = noExplicitRowHeight;
2977 } else {
2978 if (!layoutWarningIssued) {
2979 layoutWarningIssued = true;
2980 qmlWarning(me: q_func()) << "rowHeightProvider doesn't contain a function";
2981 }
2982 rowHeight = noExplicitRowHeight;
2983 }
2984
2985 cachedRowHeight.startIndex = row;
2986 cachedRowHeight.size = rowHeight;
2987 return rowHeight;
2988}
2989
2990qreal QQuickTableViewPrivate::getAlignmentContentX(int column, Qt::Alignment alignment, const qreal offset, const QRectF &subRect)
2991{
2992 Q_Q(QQuickTableView);
2993
2994 qreal contentX = 0;
2995 const int columnX = getEffectiveColumnX(column);
2996
2997 if (subRect.isValid()) {
2998 if (alignment == (Qt::AlignLeft | Qt::AlignRight)) {
2999 // Special case: Align to the right as long as the left
3000 // edge of the cell remains visible. Otherwise align to the left.
3001 alignment = subRect.width() > q->width() ? Qt::AlignLeft : Qt::AlignRight;
3002 }
3003
3004 if (alignment & Qt::AlignLeft) {
3005 contentX = columnX + subRect.x() + offset;
3006 } else if (alignment & Qt::AlignRight) {
3007 contentX = columnX + subRect.right() - viewportRect.width() + offset;
3008 } else if (alignment & Qt::AlignHCenter) {
3009 const qreal centerDistance = (viewportRect.width() - subRect.width()) / 2;
3010 contentX = columnX + subRect.x() - centerDistance + offset;
3011 }
3012 } else {
3013 const int columnWidth = getEffectiveColumnWidth(column);
3014 if (alignment == (Qt::AlignLeft | Qt::AlignRight))
3015 alignment = columnWidth > q->width() ? Qt::AlignLeft : Qt::AlignRight;
3016
3017 if (alignment & Qt::AlignLeft) {
3018 contentX = columnX + offset;
3019 } else if (alignment & Qt::AlignRight) {
3020 contentX = columnX + columnWidth - viewportRect.width() + offset;
3021 } else if (alignment & Qt::AlignHCenter) {
3022 const qreal centerDistance = (viewportRect.width() - columnWidth) / 2;
3023 contentX = columnX - centerDistance + offset;
3024 }
3025 }
3026
3027 // Don't overshoot
3028 contentX = qBound(min: -q->minXExtent(), val: contentX, max: -q->maxXExtent());
3029
3030 return contentX;
3031}
3032
3033qreal QQuickTableViewPrivate::getAlignmentContentY(int row, Qt::Alignment alignment, const qreal offset, const QRectF &subRect)
3034{
3035 Q_Q(QQuickTableView);
3036
3037 qreal contentY = 0;
3038 const int rowY = getEffectiveRowY(row);
3039
3040 if (subRect.isValid()) {
3041 if (alignment == (Qt::AlignTop | Qt::AlignBottom)) {
3042 // Special case: Align to the bottom as long as the top
3043 // edge of the cell remains visible. Otherwise align to the top.
3044 alignment = subRect.height() > q->height() ? Qt::AlignTop : Qt::AlignBottom;
3045 }
3046
3047 if (alignment & Qt::AlignTop) {
3048 contentY = rowY + subRect.y() + offset;
3049 } else if (alignment & Qt::AlignBottom) {
3050 contentY = rowY + subRect.bottom() - viewportRect.height() + offset;
3051 } else if (alignment & Qt::AlignVCenter) {
3052 const qreal centerDistance = (viewportRect.height() - subRect.height()) / 2;
3053 contentY = rowY + subRect.y() - centerDistance + offset;
3054 }
3055 } else {
3056 const int rowHeight = getEffectiveRowHeight(row);
3057 if (alignment == (Qt::AlignTop | Qt::AlignBottom))
3058 alignment = rowHeight > q->height() ? Qt::AlignTop : Qt::AlignBottom;
3059
3060 if (alignment & Qt::AlignTop) {
3061 contentY = rowY + offset;
3062 } else if (alignment & Qt::AlignBottom) {
3063 contentY = rowY + rowHeight - viewportRect.height() + offset;
3064 } else if (alignment & Qt::AlignVCenter) {
3065 const qreal centerDistance = (viewportRect.height() - rowHeight) / 2;
3066 contentY = rowY - centerDistance + offset;
3067 }
3068 }
3069
3070 // Don't overshoot
3071 contentY = qBound(min: -q->minYExtent(), val: contentY, max: -q->maxYExtent());
3072
3073 return contentY;
3074}
3075
3076bool QQuickTableViewPrivate::isColumnHidden(int column) const
3077{
3078 // A column is hidden if the width is explicit set to zero (either by
3079 // using a columnWidthProvider, or by overriding getColumnWidth()).
3080 return qFuzzyIsNull(d: getColumnWidth(column));
3081}
3082
3083bool QQuickTableViewPrivate::isRowHidden(int row) const
3084{
3085 // A row is hidden if the height is explicit set to zero (either by
3086 // using a rowHeightProvider, or by overriding getRowHeight()).
3087 return qFuzzyIsNull(d: getRowHeight(row));
3088}
3089
3090void QQuickTableViewPrivate::relayoutTableItems()
3091{
3092 qCDebug(lcTableViewDelegateLifecycle);
3093
3094 if (viewportRect.isEmpty()) {
3095 // This can happen if TableView was resized down to have a zero size
3096 qCDebug(lcTableViewDelegateLifecycle()) << "Skipping relayout, viewport has zero size";
3097 return;
3098 }
3099
3100 qreal nextColumnX = loadedTableOuterRect.x();
3101 qreal nextRowY = loadedTableOuterRect.y();
3102
3103 for (const int column : loadedColumns) {
3104 // Adjust the geometry of all cells in the current column
3105 const qreal width = getColumnLayoutWidth(column);
3106
3107 for (const int row : loadedRows) {
3108 auto item = loadedTableItem(cell: QPoint(column, row));
3109 QRectF geometry = item->geometry();
3110 geometry.moveLeft(pos: nextColumnX);
3111 geometry.setWidth(width);
3112 item->setGeometry(geometry);
3113 }
3114
3115 if (width > 0)
3116 nextColumnX += width + cellSpacing.width();
3117 }
3118
3119 for (const int row : loadedRows) {
3120 // Adjust the geometry of all cells in the current row
3121 const qreal height = getRowLayoutHeight(row);
3122
3123 for (const int column : loadedColumns) {
3124 auto item = loadedTableItem(cell: QPoint(column, row));
3125 QRectF geometry = item->geometry();
3126 geometry.moveTop(pos: nextRowY);
3127 geometry.setHeight(height);
3128 item->setGeometry(geometry);
3129 }
3130
3131 if (height > 0)
3132 nextRowY += height + cellSpacing.height();
3133 }
3134
3135 if (Q_UNLIKELY(lcTableViewDelegateLifecycle().isDebugEnabled())) {
3136 for (const int column : loadedColumns) {
3137 for (const int row : loadedRows) {
3138 QPoint cell = QPoint(column, row);
3139 qCDebug(lcTableViewDelegateLifecycle()) << "relayout item:" << cell << loadedTableItem(cell)->geometry();
3140 }
3141 }
3142 }
3143}
3144
3145void QQuickTableViewPrivate::layoutVerticalEdge(Qt::Edge tableEdge)
3146{
3147 int columnThatNeedsLayout;
3148 int neighbourColumn;
3149 qreal columnX;
3150 qreal columnWidth;
3151
3152 if (tableEdge == Qt::LeftEdge) {
3153 columnThatNeedsLayout = leftColumn();
3154 neighbourColumn = loadedColumns.values().at(i: 1);
3155 columnWidth = getColumnLayoutWidth(column: columnThatNeedsLayout);
3156 const auto neighbourItem = loadedTableItem(cell: QPoint(neighbourColumn, topRow()));
3157 columnX = neighbourItem->geometry().left() - cellSpacing.width() - columnWidth;
3158 } else {
3159 columnThatNeedsLayout = rightColumn();
3160 neighbourColumn = loadedColumns.values().at(i: loadedColumns.count() - 2);
3161 columnWidth = getColumnLayoutWidth(column: columnThatNeedsLayout);
3162 const auto neighbourItem = loadedTableItem(cell: QPoint(neighbourColumn, topRow()));
3163 columnX = neighbourItem->geometry().right() + cellSpacing.width();
3164 }
3165
3166 for (const int row : loadedRows) {
3167 auto fxTableItem = loadedTableItem(cell: QPoint(columnThatNeedsLayout, row));
3168 auto const neighbourItem = loadedTableItem(cell: QPoint(neighbourColumn, row));
3169 const qreal rowY = neighbourItem->geometry().y();
3170 const qreal rowHeight = neighbourItem->geometry().height();
3171
3172 fxTableItem->setGeometry(QRectF(columnX, rowY, columnWidth, rowHeight));
3173 fxTableItem->setVisible(true);
3174
3175 qCDebug(lcTableViewDelegateLifecycle()) << "layout item:" << QPoint(columnThatNeedsLayout, row) << fxTableItem->geometry();
3176 }
3177}
3178
3179void QQuickTableViewPrivate::layoutHorizontalEdge(Qt::Edge tableEdge)
3180{
3181 int rowThatNeedsLayout;
3182 int neighbourRow;
3183
3184 if (tableEdge == Qt::TopEdge) {
3185 rowThatNeedsLayout = topRow();
3186 neighbourRow = loadedRows.values().at(i: 1);
3187 } else {
3188 rowThatNeedsLayout = bottomRow();
3189 neighbourRow = loadedRows.values().at(i: loadedRows.count() - 2);
3190 }
3191
3192 // Set the width first, since text items in QtQuick will calculate
3193 // implicitHeight based on the text items width.
3194 for (const int column : loadedColumns) {
3195 auto fxTableItem = loadedTableItem(cell: QPoint(column, rowThatNeedsLayout));
3196 auto const neighbourItem = loadedTableItem(cell: QPoint(column, neighbourRow));
3197 const qreal columnX = neighbourItem->geometry().x();
3198 const qreal columnWidth = neighbourItem->geometry().width();
3199 fxTableItem->item->setX(columnX);
3200 fxTableItem->item->setWidth(columnWidth);
3201 }
3202
3203 qreal rowY;
3204 qreal rowHeight;
3205 if (tableEdge == Qt::TopEdge) {
3206 rowHeight = getRowLayoutHeight(row: rowThatNeedsLayout);
3207 const auto neighbourItem = loadedTableItem(cell: QPoint(leftColumn(), neighbourRow));
3208 rowY = neighbourItem->geometry().top() - cellSpacing.height() - rowHeight;
3209 } else {
3210 rowHeight = getRowLayoutHeight(row: rowThatNeedsLayout);
3211 const auto neighbourItem = loadedTableItem(cell: QPoint(leftColumn(), neighbourRow));
3212 rowY = neighbourItem->geometry().bottom() + cellSpacing.height();
3213 }
3214
3215 for (const int column : loadedColumns) {
3216 auto fxTableItem = loadedTableItem(cell: QPoint(column, rowThatNeedsLayout));
3217 fxTableItem->item->setY(rowY);
3218 fxTableItem->item->setHeight(rowHeight);
3219 fxTableItem->setVisible(true);
3220
3221 qCDebug(lcTableViewDelegateLifecycle()) << "layout item:" << QPoint(column, rowThatNeedsLayout) << fxTableItem->geometry();
3222 }
3223}
3224
3225void QQuickTableViewPrivate::layoutTopLeftItem()
3226{
3227 const QPoint cell(loadRequest.column(), loadRequest.row());
3228 auto topLeftItem = loadedTableItem(cell);
3229 auto item = topLeftItem->item;
3230
3231 item->setPosition(loadRequest.startPosition());
3232 item->setSize(QSizeF(getColumnLayoutWidth(column: cell.x()), getRowLayoutHeight(row: cell.y())));
3233 topLeftItem->setVisible(true);
3234 qCDebug(lcTableViewDelegateLifecycle) << "geometry:" << topLeftItem->geometry();
3235}
3236
3237void QQuickTableViewPrivate::layoutTableEdgeFromLoadRequest()
3238{
3239 if (loadRequest.edge() == Qt::Edge(0)) {
3240 // No edge means we're loading the top-left item
3241 layoutTopLeftItem();
3242 return;
3243 }
3244
3245 switch (loadRequest.edge()) {
3246 case Qt::LeftEdge:
3247 case Qt::RightEdge:
3248 layoutVerticalEdge(tableEdge: loadRequest.edge());
3249 break;
3250 case Qt::TopEdge:
3251 case Qt::BottomEdge:
3252 layoutHorizontalEdge(tableEdge: loadRequest.edge());
3253 break;
3254 }
3255}
3256
3257void QQuickTableViewPrivate::processLoadRequest()
3258{
3259 Q_Q(QQuickTableView);
3260 Q_TABLEVIEW_ASSERT(loadRequest.isActive(), "");
3261
3262 while (loadRequest.hasCurrentCell()) {
3263 QPoint cell = loadRequest.currentCell();
3264 FxTableItem *fxTableItem = loadFxTableItem(cell, incubationMode: loadRequest.incubationMode());
3265
3266 if (!fxTableItem) {
3267 // Requested item is not yet ready. Just leave, and wait for this
3268 // function to be called again when the item is ready.
3269 return;
3270 }
3271
3272 loadedItems.insert(key: modelIndexAtCell(cell), value: fxTableItem);
3273 loadRequest.moveToNextCell();
3274 }
3275
3276 qCDebug(lcTableViewDelegateLifecycle()) << "all items loaded!";
3277
3278 syncLoadedTableFromLoadRequest();
3279 layoutTableEdgeFromLoadRequest();
3280 syncLoadedTableRectFromLoadedTable();
3281
3282 if (rebuildState == RebuildState::Done) {
3283 // Loading of this edge was not done as a part of a rebuild, but
3284 // instead as an incremental build after e.g a flick.
3285 updateExtents();
3286 drainReusePoolAfterLoadRequest();
3287
3288 switch (loadRequest.edge()) {
3289 case Qt::LeftEdge:
3290 emit q->leftColumnChanged();
3291 break;
3292 case Qt::RightEdge:
3293 emit q->rightColumnChanged();
3294 break;
3295 case Qt::TopEdge:
3296 emit q->topRowChanged();
3297 break;
3298 case Qt::BottomEdge:
3299 emit q->bottomRowChanged();
3300 break;
3301 }
3302
3303 if (editIndex.isValid())
3304 updateEditItem();
3305
3306 emit q->layoutChanged();
3307 }
3308
3309 loadRequest.markAsDone();
3310
3311 qCDebug(lcTableViewDelegateLifecycle()) << "current table:" << tableLayoutToString();
3312 qCDebug(lcTableViewDelegateLifecycle()) << "Load request completed!";
3313 qCDebug(lcTableViewDelegateLifecycle()) << "****************************************";
3314}
3315
3316void QQuickTableViewPrivate::processRebuildTable()
3317{
3318 Q_Q(QQuickTableView);
3319
3320 if (rebuildState == RebuildState::Begin) {
3321 if (Q_UNLIKELY(lcTableViewDelegateLifecycle().isDebugEnabled())) {
3322 qCDebug(lcTableViewDelegateLifecycle()) << "begin rebuild:" << q;
3323 if (rebuildOptions & RebuildOption::All)
3324 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::All, options:" << rebuildOptions;
3325 else if (rebuildOptions & RebuildOption::ViewportOnly)
3326 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::ViewportOnly, options:" << rebuildOptions;
3327 else if (rebuildOptions & RebuildOption::LayoutOnly)
3328 qCDebug(lcTableViewDelegateLifecycle()) << "RebuildOption::LayoutOnly, options:" << rebuildOptions;
3329 else
3330 Q_TABLEVIEW_UNREACHABLE(rebuildOptions);
3331 }
3332
3333 edgesBeforeRebuild = loadedItems.isEmpty() ? QMargins()
3334 : QMargins(q->leftColumn(), q->topRow(), q->rightColumn(), q->bottomRow());
3335 }
3336
3337 moveToNextRebuildState();
3338
3339 if (rebuildState == RebuildState::LoadInitalTable) {
3340 loadInitialTable();
3341 if (!moveToNextRebuildState())
3342 return;
3343 }
3344
3345 if (rebuildState == RebuildState::VerifyTable) {
3346 if (loadedItems.isEmpty()) {
3347 qCDebug(lcTableViewDelegateLifecycle()) << "no items loaded!";
3348 updateContentWidth();
3349 updateContentHeight();
3350 rebuildState = RebuildState::Done;
3351 } else if (!moveToNextRebuildState()) {
3352 return;
3353 }
3354 }
3355
3356 if (rebuildState == RebuildState::LayoutTable) {
3357 layoutAfterLoadingInitialTable();
3358 loadAndUnloadVisibleEdges();
3359 if (!moveToNextRebuildState())
3360 return;
3361 }
3362
3363 if (rebuildState == RebuildState::CancelOvershoot) {
3364 cancelOvershootAfterLayout();
3365 loadAndUnloadVisibleEdges();
3366 if (!moveToNextRebuildState())
3367 return;
3368 }
3369
3370 if (rebuildState == RebuildState::UpdateContentSize) {
3371 updateContentSize();
3372 if (!moveToNextRebuildState())
3373 return;
3374 }
3375
3376 const bool preload = (rebuildOptions & RebuildOption::All
3377 && reusableFlag == QQmlTableInstanceModel::Reusable);
3378
3379 if (rebuildState == RebuildState::PreloadColumns) {
3380 if (preload && !atTableEnd(edge: Qt::RightEdge))
3381 loadEdge(edge: Qt::RightEdge, incubationMode: QQmlIncubator::AsynchronousIfNested);
3382 if (!moveToNextRebuildState())
3383 return;
3384 }
3385
3386 if (rebuildState == RebuildState::PreloadRows) {
3387 if (preload && !atTableEnd(edge: Qt::BottomEdge))
3388 loadEdge(edge: Qt::BottomEdge, incubationMode: QQmlIncubator::AsynchronousIfNested);
3389 if (!moveToNextRebuildState())
3390 return;
3391 }
3392
3393 if (rebuildState == RebuildState::MovePreloadedItemsToPool) {
3394 while (Qt::Edge edge = nextEdgeToUnload(rect: viewportRect))
3395 unloadEdge(edge);
3396 if (!moveToNextRebuildState())
3397 return;
3398 }
3399
3400 if (rebuildState == RebuildState::Done) {
3401 if (edgesBeforeRebuild.left() != q->leftColumn())
3402 emit q->leftColumnChanged();
3403 if (edgesBeforeRebuild.right() != q->rightColumn())
3404 emit q->rightColumnChanged();
3405 if (edgesBeforeRebuild.top() != q->topRow())
3406 emit q->topRowChanged();
3407 if (edgesBeforeRebuild.bottom() != q->bottomRow())
3408 emit q->bottomRowChanged();
3409
3410 if (editIndex.isValid())
3411 updateEditItem();
3412 updateCurrentRowAndColumn();
3413
3414 emit q->layoutChanged();
3415
3416 qCDebug(lcTableViewDelegateLifecycle()) << "current table:" << tableLayoutToString();
3417 qCDebug(lcTableViewDelegateLifecycle()) << "rebuild completed!";
3418 qCDebug(lcTableViewDelegateLifecycle()) << "################################################";
3419 qCDebug(lcTableViewDelegateLifecycle());
3420 }
3421
3422 Q_TABLEVIEW_ASSERT(rebuildState == RebuildState::Done, int(rebuildState));
3423}
3424
3425bool QQuickTableViewPrivate::moveToNextRebuildState()
3426{
3427 if (loadRequest.isActive()) {
3428 // Items are still loading async, which means
3429 // that the current state is not yet done.
3430 return false;
3431 }
3432
3433 if (rebuildState == RebuildState::Begin
3434 && rebuildOptions.testFlag(flag: RebuildOption::LayoutOnly))
3435 rebuildState = RebuildState::LayoutTable;
3436 else
3437 rebuildState = RebuildState(int(rebuildState) + 1);
3438
3439 qCDebug(lcTableViewDelegateLifecycle()) << int(rebuildState);
3440 return true;
3441}
3442
3443void QQuickTableViewPrivate::calculateTopLeft(QPoint &topLeftCell, QPointF &topLeftPos)
3444{
3445 if (tableSize.isEmpty()) {
3446 // There is no cell that can be top left
3447 topLeftCell.rx() = kEdgeIndexAtEnd;
3448 topLeftCell.ry() = kEdgeIndexAtEnd;
3449 return;
3450 }
3451
3452 if (syncHorizontally || syncVertically) {
3453 const auto syncView_d = syncView->d_func();
3454
3455 if (syncView_d->loadedItems.isEmpty()) {
3456 topLeftCell.rx() = 0;
3457 topLeftCell.ry() = 0;
3458 return;
3459 }
3460
3461 // Get sync view top left, and use that as our own top left (if possible)
3462 const QPoint syncViewTopLeftCell(syncView_d->leftColumn(), syncView_d->topRow());
3463 const auto syncViewTopLeftFxItem = syncView_d->loadedTableItem(cell: syncViewTopLeftCell);
3464 const QPointF syncViewTopLeftPos = syncViewTopLeftFxItem->geometry().topLeft();
3465
3466 if (syncHorizontally) {
3467 topLeftCell.rx() = syncViewTopLeftCell.x();
3468 topLeftPos.rx() = syncViewTopLeftPos.x();
3469
3470 if (topLeftCell.x() >= tableSize.width()) {
3471 // Top left is outside our own model.
3472 topLeftCell.rx() = kEdgeIndexAtEnd;
3473 topLeftPos.rx() = kEdgeIndexAtEnd;
3474 }
3475 }
3476
3477 if (syncVertically) {
3478 topLeftCell.ry() = syncViewTopLeftCell.y();
3479 topLeftPos.ry() = syncViewTopLeftPos.y();
3480
3481 if (topLeftCell.y() >= tableSize.height()) {
3482 // Top left is outside our own model.
3483 topLeftCell.ry() = kEdgeIndexAtEnd;
3484 topLeftPos.ry() = kEdgeIndexAtEnd;
3485 }
3486 }
3487
3488 if (syncHorizontally && syncVertically) {
3489 // We have a valid top left, so we're done
3490 return;
3491 }
3492 }
3493
3494 // Since we're not sync-ing both horizontal and vertical, calculate the missing
3495 // dimention(s) ourself. If we rebuild all, we find the first visible top-left
3496 // item starting from cell(0, 0). Otherwise, guesstimate which row or column that
3497 // should be the new top-left given the geometry of the viewport.
3498
3499 if (!syncHorizontally) {
3500 if (rebuildOptions & RebuildOption::All) {
3501 // Find the first visible column from the beginning
3502 topLeftCell.rx() = nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: 0);
3503 if (topLeftCell.x() == kEdgeIndexAtEnd) {
3504 // No visible column found
3505 return;
3506 }
3507 } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftColumn) {
3508 // Guesstimate new top left
3509 const int newColumn = int(viewportRect.x() / (averageEdgeSize.width() + cellSpacing.width()));
3510 topLeftCell.rx() = qBound(min: 0, val: newColumn, max: tableSize.width() - 1);
3511 topLeftPos.rx() = topLeftCell.x() * (averageEdgeSize.width() + cellSpacing.width());
3512 } else if (rebuildOptions & RebuildOption::PositionViewAtColumn) {
3513 topLeftCell.rx() = qBound(min: 0, val: positionViewAtColumnAfterRebuild, max: tableSize.width() - 1);
3514 topLeftPos.rx() = qFloor(v: topLeftCell.x()) * (averageEdgeSize.width() + cellSpacing.width());
3515 } else {
3516 // Keep the current top left, unless it's outside model
3517 topLeftCell.rx() = qBound(min: 0, val: leftColumn(), max: tableSize.width() - 1);
3518 // We begin by loading the columns where the viewport is at
3519 // now. But will move the whole table and the viewport
3520 // later, when we do a layoutAfterLoadingInitialTable().
3521 topLeftPos.rx() = loadedTableOuterRect.x();
3522 }
3523 }
3524
3525 if (!syncVertically) {
3526 if (rebuildOptions & RebuildOption::All) {
3527 // Find the first visible row from the beginning
3528 topLeftCell.ry() = nextVisibleEdgeIndex(edge: Qt::BottomEdge, startIndex: 0);
3529 if (topLeftCell.y() == kEdgeIndexAtEnd) {
3530 // No visible row found
3531 return;
3532 }
3533 } else if (rebuildOptions & RebuildOption::CalculateNewTopLeftRow) {
3534 // Guesstimate new top left
3535 const int newRow = int(viewportRect.y() / (averageEdgeSize.height() + cellSpacing.height()));
3536 topLeftCell.ry() = qBound(min: 0, val: newRow, max: tableSize.height() - 1);
3537 topLeftPos.ry() = topLeftCell.y() * (averageEdgeSize.height() + cellSpacing.height());
3538 } else if (rebuildOptions & RebuildOption::PositionViewAtRow) {
3539 topLeftCell.ry() = qBound(min: 0, val: positionViewAtRowAfterRebuild, max: tableSize.height() - 1);
3540 topLeftPos.ry() = qFloor(v: topLeftCell.y()) * (averageEdgeSize.height() + cellSpacing.height());
3541 } else {
3542 topLeftCell.ry() = qBound(min: 0, val: topRow(), max: tableSize.height() - 1);
3543 topLeftPos.ry() = loadedTableOuterRect.y();
3544 }
3545 }
3546}
3547
3548void QQuickTableViewPrivate::loadInitialTable()
3549{
3550 updateTableSize();
3551
3552 if (positionXAnimation.isRunning()) {
3553 positionXAnimation.stop();
3554 setLocalViewportX(positionXAnimation.to().toReal());
3555 syncViewportRect();
3556 }
3557
3558 if (positionYAnimation.isRunning()) {
3559 positionYAnimation.stop();
3560 setLocalViewportY(positionYAnimation.to().toReal());
3561 syncViewportRect();
3562 }
3563
3564 QPoint topLeft;
3565 QPointF topLeftPos;
3566 calculateTopLeft(topLeftCell&: topLeft, topLeftPos);
3567 qCDebug(lcTableViewDelegateLifecycle()) << "initial viewport rect:" << viewportRect;
3568 qCDebug(lcTableViewDelegateLifecycle()) << "initial top left cell:" << topLeft << ", pos:" << topLeftPos;
3569
3570 if (!loadedItems.isEmpty()) {
3571 if (rebuildOptions & RebuildOption::All)
3572 releaseLoadedItems(reusableFlag: QQmlTableInstanceModel::NotReusable);
3573 else if (rebuildOptions & RebuildOption::ViewportOnly)
3574 releaseLoadedItems(reusableFlag);
3575 }
3576
3577 if (rebuildOptions & RebuildOption::All) {
3578 origin = QPointF(0, 0);
3579 endExtent = QSizeF(0, 0);
3580 hData.markExtentsDirty();
3581 vData.markExtentsDirty();
3582 updateBeginningEnd();
3583 }
3584
3585 loadedColumns.clear();
3586 loadedRows.clear();
3587 loadedTableOuterRect = QRect();
3588 loadedTableInnerRect = QRect();
3589 clearEdgeSizeCache();
3590
3591 if (syncHorizontally)
3592 setLocalViewportX(syncView->contentX());
3593
3594 if (syncVertically)
3595 setLocalViewportY(syncView->contentY());
3596
3597 if (!syncHorizontally && rebuildOptions & RebuildOption::PositionViewAtColumn)
3598 setLocalViewportX(topLeftPos.x());
3599
3600 if (!syncVertically && rebuildOptions & RebuildOption::PositionViewAtRow)
3601 setLocalViewportY(topLeftPos.y());
3602
3603 syncViewportRect();
3604
3605 if (!model) {
3606 qCDebug(lcTableViewDelegateLifecycle()) << "no model found, leaving table empty";
3607 return;
3608 }
3609
3610 if (model->count() == 0) {
3611 qCDebug(lcTableViewDelegateLifecycle()) << "empty model found, leaving table empty";
3612 return;
3613 }
3614
3615 if (tableModel && !tableModel->delegate()) {
3616 qCDebug(lcTableViewDelegateLifecycle()) << "no delegate found, leaving table empty";
3617 return;
3618 }
3619
3620 if (topLeft.x() == kEdgeIndexAtEnd || topLeft.y() == kEdgeIndexAtEnd) {
3621 qCDebug(lcTableViewDelegateLifecycle()) << "no visible row or column found, leaving table empty";
3622 return;
3623 }
3624
3625 if (topLeft.x() == kEdgeIndexNotSet || topLeft.y() == kEdgeIndexNotSet) {
3626 qCDebug(lcTableViewDelegateLifecycle()) << "could not resolve top-left item, leaving table empty";
3627 return;
3628 }
3629
3630 if (viewportRect.isEmpty()) {
3631 qCDebug(lcTableViewDelegateLifecycle()) << "viewport has zero size, leaving table empty";
3632 return;
3633 }
3634
3635 // Load top-left item. After loaded, loadItemsInsideRect() will take
3636 // care of filling out the rest of the table.
3637 loadRequest.begin(cell: topLeft, pos: topLeftPos, incubationMode: QQmlIncubator::AsynchronousIfNested);
3638 processLoadRequest();
3639 loadAndUnloadVisibleEdges();
3640}
3641
3642void QQuickTableViewPrivate::updateContentSize()
3643{
3644 const bool allColumnsLoaded = atTableEnd(edge: Qt::LeftEdge) && atTableEnd(edge: Qt::RightEdge);
3645 if (rebuildOptions.testFlag(flag: RebuildOption::CalculateNewContentWidth) || allColumnsLoaded) {
3646 updateAverageColumnWidth();
3647 updateContentWidth();
3648 }
3649
3650 const bool allRowsLoaded = atTableEnd(edge: Qt::TopEdge) && atTableEnd(edge: Qt::BottomEdge);
3651 if (rebuildOptions.testFlag(flag: RebuildOption::CalculateNewContentHeight) || allRowsLoaded) {
3652 updateAverageRowHeight();
3653 updateContentHeight();
3654 }
3655
3656 updateExtents();
3657}
3658
3659void QQuickTableViewPrivate::layoutAfterLoadingInitialTable()
3660{
3661 clearEdgeSizeCache();
3662 relayoutTableItems();
3663 syncLoadedTableRectFromLoadedTable();
3664
3665 updateContentSize();
3666
3667 adjustViewportXAccordingToAlignment();
3668 adjustViewportYAccordingToAlignment();
3669}
3670
3671void QQuickTableViewPrivate::adjustViewportXAccordingToAlignment()
3672{
3673 // Check if we are supposed to position the viewport at a certain column
3674 if (!rebuildOptions.testFlag(flag: RebuildOption::PositionViewAtColumn))
3675 return;
3676 // The requested column might have been hidden or is outside model bounds
3677 if (positionViewAtColumnAfterRebuild != leftColumn())
3678 return;
3679
3680 const qreal newContentX = getAlignmentContentX(
3681 column: positionViewAtColumnAfterRebuild,
3682 alignment: positionViewAtColumnAlignment,
3683 offset: positionViewAtColumnOffset,
3684 subRect: positionViewAtColumnSubRect);
3685
3686 setLocalViewportX(newContentX);
3687 syncViewportRect();
3688}
3689
3690void QQuickTableViewPrivate::adjustViewportYAccordingToAlignment()
3691{
3692 // Check if we are supposed to position the viewport at a certain row
3693 if (!rebuildOptions.testFlag(flag: RebuildOption::PositionViewAtRow))
3694 return;
3695 // The requested row might have been hidden or is outside model bounds
3696 if (positionViewAtRowAfterRebuild != topRow())
3697 return;
3698
3699 const qreal newContentY = getAlignmentContentY(
3700 row: positionViewAtRowAfterRebuild,
3701 alignment: positionViewAtRowAlignment,
3702 offset: positionViewAtRowOffset,
3703 subRect: positionViewAtRowSubRect);
3704
3705 setLocalViewportY(newContentY);
3706 syncViewportRect();
3707}
3708
3709void QQuickTableViewPrivate::cancelOvershootAfterLayout()
3710{
3711 Q_Q(QQuickTableView);
3712
3713 // Note: we only want to cancel overshoot from a rebuild if we're supposed to position
3714 // the view on a specific cell. The app is allowed to overshoot by setting contentX and
3715 // contentY manually. Also, if this view is a sync child, we should always stay in sync
3716 // with the syncView, so then we don't do anything.
3717 const bool positionVertically = rebuildOptions.testFlag(flag: RebuildOption::PositionViewAtRow);
3718 const bool positionHorizontally = rebuildOptions.testFlag(flag: RebuildOption::PositionViewAtColumn);
3719 const bool cancelVertically = positionVertically && !syncVertically;
3720 const bool cancelHorizontally = positionHorizontally && !syncHorizontally;
3721
3722 if (cancelHorizontally && !qFuzzyIsNull(d: q->horizontalOvershoot())) {
3723 qCDebug(lcTableViewDelegateLifecycle()) << "cancelling overshoot horizontally:" << q->horizontalOvershoot();
3724 setLocalViewportX(q->horizontalOvershoot() < 0 ? -q->minXExtent() : -q->maxXExtent());
3725 syncViewportRect();
3726 }
3727
3728 if (cancelVertically && !qFuzzyIsNull(d: q->verticalOvershoot())) {
3729 qCDebug(lcTableViewDelegateLifecycle()) << "cancelling overshoot vertically:" << q->verticalOvershoot();
3730 setLocalViewportY(q->verticalOvershoot() < 0 ? -q->minYExtent() : -q->maxYExtent());
3731 syncViewportRect();
3732 }
3733}
3734
3735void QQuickTableViewPrivate::unloadEdge(Qt::Edge edge)
3736{
3737 Q_Q(QQuickTableView);
3738 qCDebug(lcTableViewDelegateLifecycle) << edge;
3739
3740 switch (edge) {
3741 case Qt::LeftEdge: {
3742 const int column = leftColumn();
3743 for (int row : loadedRows)
3744 unloadItem(cell: QPoint(column, row));
3745 loadedColumns.remove(v: column);
3746 syncLoadedTableRectFromLoadedTable();
3747 if (rebuildState == RebuildState::Done)
3748 emit q->leftColumnChanged();
3749 break; }
3750 case Qt::RightEdge: {
3751 const int column = rightColumn();
3752 for (int row : loadedRows)
3753 unloadItem(cell: QPoint(column, row));
3754 loadedColumns.remove(v: column);
3755 syncLoadedTableRectFromLoadedTable();
3756 if (rebuildState == RebuildState::Done)
3757 emit q->rightColumnChanged();
3758 break; }
3759 case Qt::TopEdge: {
3760 const int row = topRow();
3761 for (int col : loadedColumns)
3762 unloadItem(cell: QPoint(col, row));
3763 loadedRows.remove(v: row);
3764 syncLoadedTableRectFromLoadedTable();
3765 if (rebuildState == RebuildState::Done)
3766 emit q->topRowChanged();
3767 break; }
3768 case Qt::BottomEdge: {
3769 const int row = bottomRow();
3770 for (int col : loadedColumns)
3771 unloadItem(cell: QPoint(col, row));
3772 loadedRows.remove(v: row);
3773 syncLoadedTableRectFromLoadedTable();
3774 if (rebuildState == RebuildState::Done)
3775 emit q->bottomRowChanged();
3776 break; }
3777 }
3778
3779 if (rebuildState == RebuildState::Done)
3780 emit q->layoutChanged();
3781
3782 qCDebug(lcTableViewDelegateLifecycle) << tableLayoutToString();
3783}
3784
3785void QQuickTableViewPrivate::loadEdge(Qt::Edge edge, QQmlIncubator::IncubationMode incubationMode)
3786{
3787 const int edgeIndex = nextVisibleEdgeIndexAroundLoadedTable(edge);
3788 qCDebug(lcTableViewDelegateLifecycle) << edge << edgeIndex << q_func();
3789
3790 const auto &visibleCells = edge & (Qt::LeftEdge | Qt::RightEdge)
3791 ? loadedRows.values() : loadedColumns.values();
3792 loadRequest.begin(edgeToLoad: edge, edgeIndex, visibleCellsInEdge: visibleCells, incubationMode);
3793 processLoadRequest();
3794}
3795
3796void QQuickTableViewPrivate::loadAndUnloadVisibleEdges(QQmlIncubator::IncubationMode incubationMode)
3797{
3798 // Unload table edges that have been moved outside the visible part of the
3799 // table (including buffer area), and load new edges that has been moved inside.
3800 // Note: an important point is that we always keep the table rectangular
3801 // and without holes to reduce complexity (we never leave the table in
3802 // a half-loaded state, or keep track of multiple patches).
3803 // We load only one edge (row or column) at a time. This is especially
3804 // important when loading into the buffer, since we need to be able to
3805 // cancel the buffering quickly if the user starts to flick, and then
3806 // focus all further loading on the edges that are flicked into view.
3807
3808 if (loadRequest.isActive()) {
3809 // Don't start loading more edges while we're
3810 // already waiting for another one to load.
3811 return;
3812 }
3813
3814 if (loadedItems.isEmpty()) {
3815 // We need at least the top-left item to be loaded before we can
3816 // start loading edges around it. Not having a top-left item at
3817 // this point means that the model is empty (or no delegate).
3818 return;
3819 }
3820
3821 bool tableModified;
3822
3823 do {
3824 tableModified = false;
3825
3826 if (Qt::Edge edge = nextEdgeToUnload(rect: viewportRect)) {
3827 tableModified = true;
3828 unloadEdge(edge);
3829 }
3830
3831 if (Qt::Edge edge = nextEdgeToLoad(rect: viewportRect)) {
3832 tableModified = true;
3833 loadEdge(edge, incubationMode);
3834 if (loadRequest.isActive())
3835 return;
3836 }
3837 } while (tableModified);
3838
3839}
3840
3841void QQuickTableViewPrivate::drainReusePoolAfterLoadRequest()
3842{
3843 Q_Q(QQuickTableView);
3844
3845 if (reusableFlag == QQmlTableInstanceModel::NotReusable || !tableModel)
3846 return;
3847
3848 if (!qFuzzyIsNull(d: q->verticalOvershoot()) || !qFuzzyIsNull(d: q->horizontalOvershoot())) {
3849 // Don't drain while we're overshooting, since this will fill up the
3850 // pool, but we expect to reuse them all once the content item moves back.
3851 return;
3852 }
3853
3854 // When loading edges, we don't want to drain the reuse pool too aggressively. Normally,
3855 // all the items in the pool are reused rapidly as the content view is flicked around
3856 // anyway. Even if the table is temporarily flicked to a section that contains fewer
3857 // cells than what used to be (e.g if the flicked-in rows are taller than average), it
3858 // still makes sense to keep all the items in circulation; Chances are, that soon enough,
3859 // thinner rows are flicked back in again (meaning that we can fit more items into the
3860 // view). But at the same time, if a delegate chooser is in use, the pool might contain
3861 // items created from different delegates. And some of those delegates might be used only
3862 // occasionally. So to avoid situations where an item ends up in the pool for too long, we
3863 // call drain after each load request, but with a sufficiently large pool time. (If an item
3864 // in the pool has a large pool time, it means that it hasn't been reused for an equal
3865 // amount of load cycles, and should be released).
3866 //
3867 // We calculate an appropriate pool time by figuring out what the minimum time must be to
3868 // not disturb frequently reused items. Since the number of items in a row might be higher
3869 // than in a column (or vice versa), the minimum pool time should take into account that
3870 // you might be flicking out a single row (filling up the pool), before you continue
3871 // flicking in several new columns (taking them out again, but now in smaller chunks). This
3872 // will increase the number of load cycles items are kept in the pool (poolTime), but still,
3873 // we shouldn't release them, as they are still being reused frequently.
3874 // To get a flexible maxValue (that e.g tolerates rows and columns being flicked
3875 // in with varying sizes, causing some items not to be resued immediately), we multiply the
3876 // value by 2. Note that we also add an extra +1 to the column count, because the number of
3877 // visible columns will fluctuate between +1/-1 while flicking.
3878 const int w = loadedColumns.count();
3879 const int h = loadedRows.count();
3880 const int minTime = int(std::ceil(x: w > h ? qreal(w + 1) / h : qreal(h + 1) / w));
3881 const int maxTime = minTime * 2;
3882 tableModel->drainReusableItemsPool(maxPoolTime: maxTime);
3883}
3884
3885void QQuickTableViewPrivate::scheduleRebuildTable(RebuildOptions options) {
3886 if (!q_func()->isComponentComplete()) {
3887 // We'll rebuild the table once complete anyway
3888 return;
3889 }
3890
3891 scheduledRebuildOptions |= options;
3892 q_func()->polish();
3893}
3894
3895QQuickTableView *QQuickTableViewPrivate::rootSyncView() const
3896{
3897 QQuickTableView *root = const_cast<QQuickTableView *>(q_func());
3898 while (QQuickTableView *view = root->d_func()->syncView)
3899 root = view;
3900 return root;
3901}
3902
3903void QQuickTableViewPrivate::updatePolish()
3904{
3905 // We always start updating from the top of the syncView tree, since
3906 // the layout of a syncView child will depend on the layout of the syncView.
3907 // E.g when a new column is flicked in, the syncView should load and layout
3908 // the column first, before any syncChildren gets a chance to do the same.
3909 Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!");
3910 rootSyncView()->d_func()->updateTableRecursive();
3911}
3912
3913bool QQuickTableViewPrivate::updateTableRecursive()
3914{
3915 if (polishing) {
3916 // We're already updating the Table in this view, so
3917 // we cannot continue. Signal this back by returning false.
3918 // The caller can then choose to call "polish()" instead, to
3919 // do the update later.
3920 return false;
3921 }
3922
3923 const bool updateComplete = updateTable();
3924 if (!updateComplete)
3925 return false;
3926
3927 const auto children = syncChildren;
3928 for (auto syncChild : children) {
3929 auto syncChild_d = syncChild->d_func();
3930 const int mask =
3931 RebuildOption::PositionViewAtRow |
3932 RebuildOption::PositionViewAtColumn |
3933 RebuildOption::CalculateNewTopLeftRow |
3934 RebuildOption::CalculateNewTopLeftColumn;
3935 syncChild_d->scheduledRebuildOptions |= rebuildOptions & ~mask;
3936
3937 const bool descendantUpdateComplete = syncChild_d->updateTableRecursive();
3938 if (!descendantUpdateComplete)
3939 return false;
3940 }
3941
3942 rebuildOptions = RebuildOption::None;
3943
3944 return true;
3945}
3946
3947bool QQuickTableViewPrivate::updateTable()
3948{
3949 // Whenever something changes, e.g viewport moves, spacing is set to a
3950 // new value, model changes etc, this function will end up being called. Here
3951 // we check what needs to be done, and load/unload cells accordingly.
3952 // If we cannot complete the update (because we need to wait for an item
3953 // to load async), we return false.
3954
3955 Q_TABLEVIEW_ASSERT(!polishing, "recursive updatePolish() calls are not allowed!");
3956 QBoolBlocker polishGuard(polishing, true);
3957
3958 if (loadRequest.isActive()) {
3959 // We're currently loading items async to build a new edge in the table. We see the loading
3960 // as an atomic operation, which means that we don't continue doing anything else until all
3961 // items have been received and laid out. Note that updatePolish is then called once more
3962 // after the loadRequest has completed to handle anything that might have occurred in-between.
3963 return false;
3964 }
3965
3966 if (rebuildState != RebuildState::Done) {
3967 processRebuildTable();
3968 return rebuildState == RebuildState::Done;
3969 }
3970
3971 syncWithPendingChanges();
3972
3973 if (rebuildState == RebuildState::Begin) {
3974 processRebuildTable();
3975 return rebuildState == RebuildState::Done;
3976 }
3977
3978 if (loadedItems.isEmpty())
3979 return !loadRequest.isActive();
3980
3981 loadAndUnloadVisibleEdges();
3982 updateEditItem();
3983
3984 return !loadRequest.isActive();
3985}
3986
3987void QQuickTableViewPrivate::fixup(QQuickFlickablePrivate::AxisData &data, qreal minExtent, qreal maxExtent)
3988{
3989 if (inUpdateContentSize) {
3990 // We update the content size dynamically as we load and unload edges.
3991 // Unfortunately, this also triggers a call to this function. The base
3992 // implementation will do things like start a momentum animation or move
3993 // the content view somewhere else, which causes glitches. This can
3994 // especially happen if flicking on one of the syncView children, which triggers
3995 // an update to our content size. In that case, the base implementation don't know
3996 // that the view is being indirectly dragged, and will therefore do strange things as
3997 // it tries to 'fixup' the geometry. So we use a guard to prevent this from happening.
3998 return;
3999 }
4000
4001 QQuickFlickablePrivate::fixup(data, minExtent, maxExtent);
4002}
4003
4004QTypeRevision QQuickTableViewPrivate::resolveImportVersion()
4005{
4006 const auto data = QQmlData::get(object: q_func());
4007 if (!data || !data->propertyCache)
4008 return QTypeRevision::zero();
4009
4010 const auto cppMetaObject = data->propertyCache->firstCppMetaObject();
4011 const auto qmlTypeView = QQmlMetaType::qmlType(cppMetaObject);
4012
4013 // TODO: did we rather want qmlTypeView.revision() here?
4014 return qmlTypeView.metaObjectRevision();
4015}
4016
4017void QQuickTableViewPrivate::createWrapperModel()
4018{
4019 Q_Q(QQuickTableView);
4020 // When the assigned model is not an instance model, we create a wrapper
4021 // model (QQmlTableInstanceModel) that keeps a pointer to both the
4022 // assigned model and the assigned delegate. This model will give us a
4023 // common interface to any kind of model (js arrays, QAIM, number etc), and
4024 // help us create delegate instances.
4025 tableModel = new QQmlTableInstanceModel(qmlContext(q));
4026 tableModel->useImportVersion(version: resolveImportVersion());
4027 model = tableModel;
4028}
4029
4030bool QQuickTableViewPrivate::selectedInSelectionModel(const QPoint &cell) const
4031{
4032 if (!selectionModel)
4033 return false;
4034
4035 QAbstractItemModel *model = selectionModel->model();
4036 if (!model)
4037 return false;
4038
4039 return selectionModel->isSelected(index: q_func()->modelIndex(cell));
4040}
4041
4042bool QQuickTableViewPrivate::currentInSelectionModel(const QPoint &cell) const
4043{
4044 if (!selectionModel)
4045 return false;
4046
4047 QAbstractItemModel *model = selectionModel->model();
4048 if (!model)
4049 return false;
4050
4051 return selectionModel->currentIndex() == q_func()->modelIndex(cell);
4052}
4053
4054void QQuickTableViewPrivate::selectionChangedInSelectionModel(const QItemSelection &selected, const QItemSelection &deselected)
4055{
4056 if (!selectionModel->hasSelection()) {
4057 // Ensure that we cancel any ongoing key/mouse-based selections
4058 // if selectionModel.clearSelection() is called.
4059 clearSelection();
4060 }
4061
4062 const auto &selectedIndexes = selected.indexes();
4063 const auto &deselectedIndexes = deselected.indexes();
4064 for (int i = 0; i < selectedIndexes.size(); ++i)
4065 setSelectedOnDelegateItem(modelIndex: selectedIndexes.at(i), select: true);
4066 for (int i = 0; i < deselectedIndexes.size(); ++i)
4067 setSelectedOnDelegateItem(modelIndex: deselectedIndexes.at(i), select: false);
4068}
4069
4070void QQuickTableViewPrivate::setSelectedOnDelegateItem(const QModelIndex &modelIndex, bool select)
4071{
4072 const int cellIndex = modelIndexToCellIndex(modelIndex);
4073 if (!loadedItems.contains(key: cellIndex))
4074 return;
4075 const QPoint cell = cellAtModelIndex(modelIndex: cellIndex);
4076 QQuickItem *item = loadedTableItem(cell)->item;
4077 setRequiredProperty(property: kRequiredProperty_selected, value: QVariant::fromValue(value: select), serializedModelIndex: cellIndex, object: item, init: false);
4078}
4079
4080QAbstractItemModel *QQuickTableViewPrivate::qaim(QVariant modelAsVariant) const
4081{
4082 // If modelAsVariant wraps a qaim, return it
4083 if (modelAsVariant.userType() == qMetaTypeId<QJSValue>())
4084 modelAsVariant = modelAsVariant.value<QJSValue>().toVariant();
4085 return qvariant_cast<QAbstractItemModel *>(v: modelAsVariant);
4086}
4087
4088void QQuickTableViewPrivate::updateSelectedOnAllDelegateItems()
4089{
4090 updateCurrentRowAndColumn();
4091
4092 for (auto it = loadedItems.keyBegin(), end = loadedItems.keyEnd(); it != end; ++it) {
4093 const int cellIndex = *it;
4094 const QPoint cell = cellAtModelIndex(modelIndex: cellIndex);
4095 const bool selected = selectedInSelectionModel(cell);
4096 const bool current = currentInSelectionModel(cell);
4097 QQuickItem *item = loadedTableItem(cell)->item;
4098 const bool editing = editIndex == q_func()->modelIndex(cell);
4099 setRequiredProperty(property: kRequiredProperty_selected, value: QVariant::fromValue(value: selected), serializedModelIndex: cellIndex, object: item, init: false);
4100 setRequiredProperty(property: kRequiredProperty_current, value: QVariant::fromValue(value: current), serializedModelIndex: cellIndex, object: item, init: false);
4101 setRequiredProperty(property: kRequiredProperty_editing, value: QVariant::fromValue(value: editing), serializedModelIndex: cellIndex, object: item, init: false);
4102 }
4103}
4104
4105void QQuickTableViewPrivate::currentChangedInSelectionModel(const QModelIndex &current, const QModelIndex &previous)
4106{
4107 // Warn if the source models are not the same
4108 const QAbstractItemModel *qaimInSelection = selectionModel ? selectionModel->model() : nullptr;
4109 const QAbstractItemModel *qaimInTableView = qaim(modelAsVariant: modelImpl());
4110 if (qaimInSelection && qaimInSelection != qaimInTableView)
4111 qmlWarning(me: q_func()) << "TableView.selectionModel.model differs from TableView.model";
4112
4113 updateCurrentRowAndColumn();
4114 setCurrentOnDelegateItem(index: previous, isCurrent: false);
4115 setCurrentOnDelegateItem(index: current, isCurrent: true);
4116}
4117
4118void QQuickTableViewPrivate::updateCurrentRowAndColumn()
4119{
4120 Q_Q(QQuickTableView);
4121
4122 const QModelIndex currentIndex = selectionModel ? selectionModel->currentIndex() : QModelIndex();
4123 const QPoint currentCell = q->cellAtIndex(index: currentIndex);
4124 if (currentCell.x() != currentColumn) {
4125 currentColumn = currentCell.x();
4126 emit q->currentColumnChanged();
4127 }
4128
4129 if (currentCell.y() != currentRow) {
4130 currentRow = currentCell.y();
4131 emit q->currentRowChanged();
4132 }
4133}
4134
4135void QQuickTableViewPrivate::setCurrentOnDelegateItem(const QModelIndex &index, bool isCurrent)
4136{
4137 const int cellIndex = modelIndexToCellIndex(modelIndex: index);
4138 if (!loadedItems.contains(key: cellIndex))
4139 return;
4140
4141 const QPoint cell = cellAtModelIndex(modelIndex: cellIndex);
4142 QQuickItem *item = loadedTableItem(cell)->item;
4143 setRequiredProperty(property: kRequiredProperty_current, value: QVariant::fromValue(value: isCurrent), serializedModelIndex: cellIndex, object: item, init: false);
4144}
4145
4146void QQuickTableViewPrivate::itemCreatedCallback(int modelIndex, QObject*)
4147{
4148 if (blockItemCreatedCallback)
4149 return;
4150
4151 qCDebug(lcTableViewDelegateLifecycle) << "item done loading:"
4152 << cellAtModelIndex(modelIndex);
4153
4154 // Since the item we waited for has finished incubating, we can
4155 // continue with the load request. processLoadRequest will
4156 // ask the model for the requested item once more, which will be
4157 // quick since the model has cached it.
4158 processLoadRequest();
4159 loadAndUnloadVisibleEdges();
4160 updatePolish();
4161}
4162
4163void QQuickTableViewPrivate::initItemCallback(int modelIndex, QObject *object)
4164{
4165 Q_Q(QQuickTableView);
4166
4167 auto item = qobject_cast<QQuickItem*>(o: object);
4168 if (!item)
4169 return;
4170
4171 item->setParentItem(q->contentItem());
4172 item->setZ(1);
4173
4174 const QPoint cell = cellAtModelIndex(modelIndex);
4175 const bool current = currentInSelectionModel(cell);
4176 const bool selected = selectedInSelectionModel(cell);
4177 setRequiredProperty(property: kRequiredProperty_current, value: QVariant::fromValue(value: current), serializedModelIndex: modelIndex, object, init: true);
4178 setRequiredProperty(property: kRequiredProperty_selected, value: QVariant::fromValue(value: selected), serializedModelIndex: modelIndex, object, init: true);
4179 setRequiredProperty(property: kRequiredProperty_editing, value: QVariant::fromValue(value: false), serializedModelIndex: modelIndex, object: item, init: true);
4180
4181 if (auto attached = getAttachedObject(object))
4182 attached->setView(q);
4183}
4184
4185void QQuickTableViewPrivate::itemPooledCallback(int modelIndex, QObject *object)
4186{
4187 Q_UNUSED(modelIndex);
4188
4189 if (auto attached = getAttachedObject(object))
4190 emit attached->pooled();
4191}
4192
4193void QQuickTableViewPrivate::itemReusedCallback(int modelIndex, QObject *object)
4194{
4195 const QPoint cell = cellAtModelIndex(modelIndex);
4196 const bool current = currentInSelectionModel(cell);
4197 const bool selected = selectedInSelectionModel(cell);
4198 setRequiredProperty(property: kRequiredProperty_current, value: QVariant::fromValue(value: current), serializedModelIndex: modelIndex, object, init: false);
4199 setRequiredProperty(property: kRequiredProperty_selected, value: QVariant::fromValue(value: selected), serializedModelIndex: modelIndex, object, init: false);
4200 // Note: the edit item will never be reused, so no reason to set kRequiredProperty_editing
4201
4202 if (auto item = qobject_cast<QQuickItem*>(o: object))
4203 QQuickItemPrivate::get(item)->setCulled(false);
4204
4205 if (auto attached = getAttachedObject(object))
4206 emit attached->reused();
4207}
4208
4209void QQuickTableViewPrivate::syncWithPendingChanges()
4210{
4211 // The application can change properties like the model or the delegate while
4212 // we're e.g in the middle of e.g loading a new row. Since this will lead to
4213 // unpredicted behavior, and possibly a crash, we need to postpone taking
4214 // such assignments into effect until we're in a state that allows it.
4215
4216 syncViewportRect();
4217 syncModel();
4218 syncDelegate();
4219 syncSyncView();
4220 syncPositionView();
4221
4222 syncRebuildOptions();
4223}
4224
4225void QQuickTableViewPrivate::syncRebuildOptions()
4226{
4227 if (!scheduledRebuildOptions)
4228 return;
4229
4230 rebuildState = RebuildState::Begin;
4231 rebuildOptions = scheduledRebuildOptions;
4232 scheduledRebuildOptions = RebuildOption::None;
4233
4234 if (loadedItems.isEmpty())
4235 rebuildOptions.setFlag(flag: RebuildOption::All);
4236
4237 // Some options are exclusive:
4238 if (rebuildOptions.testFlag(flag: RebuildOption::All)) {
4239 rebuildOptions.setFlag(flag: RebuildOption::ViewportOnly, on: false);
4240 rebuildOptions.setFlag(flag: RebuildOption::LayoutOnly, on: false);
4241 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewContentWidth);
4242 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewContentHeight);
4243 } else if (rebuildOptions.testFlag(flag: RebuildOption::ViewportOnly)) {
4244 rebuildOptions.setFlag(flag: RebuildOption::LayoutOnly, on: false);
4245 }
4246
4247 if (rebuildOptions.testFlag(flag: RebuildOption::PositionViewAtRow))
4248 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftRow, on: false);
4249
4250 if (rebuildOptions.testFlag(flag: RebuildOption::PositionViewAtColumn))
4251 rebuildOptions.setFlag(flag: RebuildOption::CalculateNewTopLeftColumn, on: false);
4252}
4253
4254void QQuickTableViewPrivate::syncDelegate()
4255{
4256 if (!tableModel) {
4257 // Only the tableModel uses the delegate assigned to a
4258 // TableView. DelegateModel has it's own delegate, and
4259 // ObjectModel etc. doesn't use one.
4260 return;
4261 }
4262
4263 if (assignedDelegate != tableModel->delegate())
4264 tableModel->setDelegate(assignedDelegate);
4265}
4266
4267QVariant QQuickTableViewPrivate::modelImpl() const
4268{
4269 return assignedModel;
4270}
4271
4272void QQuickTableViewPrivate::setModelImpl(const QVariant &newModel)
4273{
4274 if (newModel == assignedModel)
4275 return;
4276
4277 assignedModel = newModel;
4278 scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::All);
4279 emit q_func()->modelChanged();
4280}
4281
4282void QQuickTableViewPrivate::syncModel()
4283{
4284 if (modelVariant == assignedModel)
4285 return;
4286
4287 if (model) {
4288 disconnectFromModel();
4289 releaseLoadedItems(reusableFlag: QQmlTableInstanceModel::NotReusable);
4290 }
4291
4292 modelVariant = assignedModel;
4293 QVariant effectiveModelVariant = modelVariant;
4294 if (effectiveModelVariant.userType() == qMetaTypeId<QJSValue>())
4295 effectiveModelVariant = effectiveModelVariant.value<QJSValue>().toVariant();
4296
4297 const auto instanceModel = qobject_cast<QQmlInstanceModel *>(object: qvariant_cast<QObject*>(v: effectiveModelVariant));
4298
4299 if (instanceModel) {
4300 if (tableModel) {
4301 delete tableModel;
4302 tableModel = nullptr;
4303 }
4304 model = instanceModel;
4305 } else {
4306 if (!tableModel)
4307 createWrapperModel();
4308 tableModel->setModel(effectiveModelVariant);
4309 }
4310
4311 connectToModel();
4312}
4313
4314void QQuickTableViewPrivate::syncSyncView()
4315{
4316 Q_Q(QQuickTableView);
4317
4318 if (assignedSyncView != syncView) {
4319 if (syncView)
4320 syncView->d_func()->syncChildren.removeOne(t: q);
4321
4322 if (assignedSyncView) {
4323 QQuickTableView *view = assignedSyncView;
4324
4325 while (view) {
4326 if (view == q) {
4327 if (!layoutWarningIssued) {
4328 layoutWarningIssued = true;
4329 qmlWarning(me: q) << "TableView: recursive syncView connection detected!";
4330 }
4331 syncView = nullptr;
4332 return;
4333 }
4334 view = view->d_func()->syncView;
4335 }
4336
4337 assignedSyncView->d_func()->syncChildren.append(t: q);
4338 scheduledRebuildOptions |= RebuildOption::ViewportOnly;
4339 }
4340
4341 syncView = assignedSyncView;
4342 }
4343
4344 syncHorizontally = syncView && assignedSyncDirection & Qt::Horizontal;
4345 syncVertically = syncView && assignedSyncDirection & Qt::Vertical;
4346
4347 if (syncHorizontally) {
4348 QBoolBlocker fixupGuard(inUpdateContentSize, true);
4349 q->setColumnSpacing(syncView->columnSpacing());
4350 q->setLeftMargin(syncView->leftMargin());
4351 q->setRightMargin(syncView->rightMargin());
4352 updateContentWidth();
4353
4354 if (syncView->leftColumn() != q->leftColumn()) {
4355 // The left column is no longer the same as the left
4356 // column in syncView. This requires a rebuild.
4357 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftColumn;
4358 scheduledRebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
4359 }
4360 }
4361
4362 if (syncVertically) {
4363 QBoolBlocker fixupGuard(inUpdateContentSize, true);
4364 q->setRowSpacing(syncView->rowSpacing());
4365 q->setTopMargin(syncView->topMargin());
4366 q->setBottomMargin(syncView->bottomMargin());
4367 updateContentHeight();
4368
4369 if (syncView->topRow() != q->topRow()) {
4370 // The top row is no longer the same as the top
4371 // row in syncView. This requires a rebuild.
4372 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::CalculateNewTopLeftRow;
4373 scheduledRebuildOptions.setFlag(flag: RebuildOption::ViewportOnly);
4374 }
4375 }
4376
4377 if (syncView && loadedItems.isEmpty() && !tableSize.isEmpty()) {
4378 // When we have a syncView, we can sometimes temporarily end up with no loaded items.
4379 // This can happen if the syncView has a model with more rows or columns than us, in
4380 // which case the viewport can end up in a place where we have no rows or columns to
4381 // show. In that case, check now if the viewport has been flicked back again, and
4382 // that we can rebuild the table with a visible top-left cell.
4383 const auto syncView_d = syncView->d_func();
4384 if (!syncView_d->loadedItems.isEmpty()) {
4385 if (syncHorizontally && syncView_d->leftColumn() <= tableSize.width() - 1)
4386 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::ViewportOnly;
4387 else if (syncVertically && syncView_d->topRow() <= tableSize.height() - 1)
4388 scheduledRebuildOptions |= QQuickTableViewPrivate::RebuildOption::ViewportOnly;
4389 }
4390 }
4391}
4392
4393void QQuickTableViewPrivate::syncPositionView()
4394{
4395 // Only positionViewAtRowAfterRebuild/positionViewAtColumnAfterRebuild are critical
4396 // to sync before a rebuild to avoid them being overwritten
4397 // by the setters while building. The other position properties
4398 // can change without it causing trouble.
4399 positionViewAtRowAfterRebuild = assignedPositionViewAtRowAfterRebuild;
4400 positionViewAtColumnAfterRebuild = assignedPositionViewAtColumnAfterRebuild;
4401}
4402
4403void QQuickTableViewPrivate::connectToModel()
4404{
4405 Q_Q(QQuickTableView);
4406 Q_TABLEVIEW_ASSERT(model, "");
4407
4408 QObjectPrivate::connect(sender: model, signal: &QQmlInstanceModel::createdItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemCreatedCallback);
4409 QObjectPrivate::connect(sender: model, signal: &QQmlInstanceModel::initItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::initItemCallback);
4410 QObjectPrivate::connect(sender: model, signal: &QQmlTableInstanceModel::itemPooled, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemPooledCallback);
4411 QObjectPrivate::connect(sender: model, signal: &QQmlTableInstanceModel::itemReused, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemReusedCallback);
4412
4413 // Connect atYEndChanged to a function that fetches data if more is available
4414 QObjectPrivate::connect(sender: q, signal: &QQuickTableView::atYEndChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::fetchMoreData);
4415
4416 if (auto const aim = model->abstractItemModel()) {
4417 // When the model exposes a QAIM, we connect to it directly. This means that if the current model is
4418 // a QQmlDelegateModel, we just ignore all the change sets it emits. In most cases, the model will instead
4419 // be our own QQmlTableInstanceModel, which doesn't bother creating change sets at all. For models that are
4420 // not based on QAIM (like QQmlObjectModel, QQmlListModel, javascript arrays etc), there is currently no way
4421 // to modify the model at runtime without also re-setting the model on the view.
4422 connect(sender: aim, signal: &QAbstractItemModel::rowsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsMovedCallback);
4423 connect(sender: aim, signal: &QAbstractItemModel::columnsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsMovedCallback);
4424 connect(sender: aim, signal: &QAbstractItemModel::rowsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsInsertedCallback);
4425 connect(sender: aim, signal: &QAbstractItemModel::rowsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsRemovedCallback);
4426 connect(sender: aim, signal: &QAbstractItemModel::columnsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsInsertedCallback);
4427 connect(sender: aim, signal: &QAbstractItemModel::columnsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsRemovedCallback);
4428 connect(sender: aim, signal: &QAbstractItemModel::modelReset, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelResetCallback);
4429 connect(sender: aim, signal: &QAbstractItemModel::layoutChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::layoutChangedCallback);
4430 } else {
4431 QObjectPrivate::connect(sender: model, signal: &QQmlInstanceModel::modelUpdated, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelUpdated);
4432 }
4433}
4434
4435void QQuickTableViewPrivate::disconnectFromModel()
4436{
4437 Q_Q(QQuickTableView);
4438 Q_TABLEVIEW_ASSERT(model, "");
4439
4440 QObjectPrivate::disconnect(sender: model, signal: &QQmlInstanceModel::createdItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemCreatedCallback);
4441 QObjectPrivate::disconnect(sender: model, signal: &QQmlInstanceModel::initItem, receiverPrivate: this, slot: &QQuickTableViewPrivate::initItemCallback);
4442 QObjectPrivate::disconnect(sender: model, signal: &QQmlTableInstanceModel::itemPooled, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemPooledCallback);
4443 QObjectPrivate::disconnect(sender: model, signal: &QQmlTableInstanceModel::itemReused, receiverPrivate: this, slot: &QQuickTableViewPrivate::itemReusedCallback);
4444
4445 QObjectPrivate::disconnect(sender: q, signal: &QQuickTableView::atYEndChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::fetchMoreData);
4446
4447 if (auto const aim = model->abstractItemModel()) {
4448 disconnect(sender: aim, signal: &QAbstractItemModel::rowsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsMovedCallback);
4449 disconnect(sender: aim, signal: &QAbstractItemModel::columnsMoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsMovedCallback);
4450 disconnect(sender: aim, signal: &QAbstractItemModel::rowsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsInsertedCallback);
4451 disconnect(sender: aim, signal: &QAbstractItemModel::rowsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::rowsRemovedCallback);
4452 disconnect(sender: aim, signal: &QAbstractItemModel::columnsInserted, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsInsertedCallback);
4453 disconnect(sender: aim, signal: &QAbstractItemModel::columnsRemoved, receiverPrivate: this, slot: &QQuickTableViewPrivate::columnsRemovedCallback);
4454 disconnect(sender: aim, signal: &QAbstractItemModel::modelReset, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelResetCallback);
4455 disconnect(sender: aim, signal: &QAbstractItemModel::layoutChanged, receiverPrivate: this, slot: &QQuickTableViewPrivate::layoutChangedCallback);
4456 } else {
4457 QObjectPrivate::disconnect(sender: model, signal: &QQmlInstanceModel::modelUpdated, receiverPrivate: this, slot: &QQuickTableViewPrivate::modelUpdated);
4458 }
4459}
4460
4461void QQuickTableViewPrivate::modelUpdated(const QQmlChangeSet &changeSet, bool reset)
4462{
4463 Q_UNUSED(changeSet);
4464 Q_UNUSED(reset);
4465
4466 Q_TABLEVIEW_ASSERT(!model->abstractItemModel(), "");
4467 scheduleRebuildTable(options: RebuildOption::ViewportOnly
4468 | RebuildOption::CalculateNewContentWidth
4469 | RebuildOption::CalculateNewContentHeight);
4470}
4471
4472void QQuickTableViewPrivate::rowsMovedCallback(const QModelIndex &parent, int, int, const QModelIndex &, int )
4473{
4474 if (parent != QModelIndex())
4475 return;
4476
4477 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
4478}
4479
4480void QQuickTableViewPrivate::columnsMovedCallback(const QModelIndex &parent, int, int, const QModelIndex &, int)
4481{
4482 if (parent != QModelIndex())
4483 return;
4484
4485 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
4486}
4487
4488void QQuickTableViewPrivate::rowsInsertedCallback(const QModelIndex &parent, int, int)
4489{
4490 if (parent != QModelIndex())
4491 return;
4492
4493 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
4494}
4495
4496void QQuickTableViewPrivate::rowsRemovedCallback(const QModelIndex &parent, int, int)
4497{
4498 Q_Q(QQuickTableView);
4499
4500 if (parent != QModelIndex())
4501 return;
4502
4503 // If editIndex was a part of the removed rows, it will now be invalid.
4504 if (!editIndex.isValid() && editItem)
4505 q->closeEditor();
4506
4507 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
4508}
4509
4510void QQuickTableViewPrivate::columnsInsertedCallback(const QModelIndex &parent, int, int)
4511{
4512 if (parent != QModelIndex())
4513 return;
4514
4515 // Adding a column (or row) can result in the table going from being
4516 // e.g completely inside the viewport to go outside. And in the latter
4517 // case, the user needs to be able to scroll the viewport, also if
4518 // flags such as Flickable.StopAtBounds is in use. So we need to
4519 // update contentWidth to support that case.
4520 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
4521}
4522
4523void QQuickTableViewPrivate::columnsRemovedCallback(const QModelIndex &parent, int, int)
4524{
4525 Q_Q(QQuickTableView);
4526
4527 if (parent != QModelIndex())
4528 return;
4529
4530 // If editIndex was a part of the removed columns, it will now be invalid.
4531 if (!editIndex.isValid() && editItem)
4532 q->closeEditor();
4533
4534 scheduleRebuildTable(options: RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
4535}
4536
4537void QQuickTableViewPrivate::layoutChangedCallback(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint)
4538{
4539 Q_UNUSED(parents);
4540 Q_UNUSED(hint);
4541
4542 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
4543}
4544
4545void QQuickTableViewPrivate::fetchMoreData()
4546{
4547 if (tableModel && tableModel->canFetchMore()) {
4548 tableModel->fetchMore();
4549 scheduleRebuildTable(options: RebuildOption::ViewportOnly);
4550 }
4551}
4552
4553void QQuickTableViewPrivate::modelResetCallback()
4554{
4555 Q_Q(QQuickTableView);
4556 q->closeEditor();
4557 scheduleRebuildTable(options: RebuildOption::All);
4558}
4559
4560void QQuickTableViewPrivate::positionViewAtRow(int row, Qt::Alignment alignment, qreal offset, const QRectF subRect)
4561{
4562 Qt::Alignment verticalAlignment = alignment & (Qt::AlignTop | Qt::AlignVCenter | Qt::AlignBottom);
4563 Q_TABLEVIEW_ASSERT(verticalAlignment, alignment);
4564
4565 if (syncHorizontally) {
4566 syncView->d_func()->positionViewAtRow(row, alignment: verticalAlignment, offset, subRect);
4567 } else {
4568 if (!scrollToRow(row, alignment: verticalAlignment, offset, subRect)) {
4569 // Could not scroll, so rebuild instead
4570 assignedPositionViewAtRowAfterRebuild = row;
4571 positionViewAtRowAlignment = verticalAlignment;
4572 positionViewAtRowOffset = offset;
4573 positionViewAtRowSubRect = subRect;
4574 scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly |
4575 QQuickTableViewPrivate::RebuildOption::PositionViewAtRow);
4576 }
4577 }
4578}
4579
4580void QQuickTableViewPrivate::positionViewAtColumn(int column, Qt::Alignment alignment, qreal offset, const QRectF subRect)
4581{
4582 Qt::Alignment horizontalAlignment = alignment & (Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight);
4583 Q_TABLEVIEW_ASSERT(horizontalAlignment, alignment);
4584
4585 if (syncVertically) {
4586 syncView->d_func()->positionViewAtColumn(column, alignment: horizontalAlignment, offset, subRect);
4587 } else {
4588 if (!scrollToColumn(column, alignment: horizontalAlignment, offset, subRect)) {
4589 // Could not scroll, so rebuild instead
4590 assignedPositionViewAtColumnAfterRebuild = column;
4591 positionViewAtColumnAlignment = horizontalAlignment;
4592 positionViewAtColumnOffset = offset;
4593 positionViewAtColumnSubRect = subRect;
4594 scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly |
4595 QQuickTableViewPrivate::RebuildOption::PositionViewAtColumn);
4596 }
4597 }
4598}
4599
4600bool QQuickTableViewPrivate::scrollToRow(int row, Qt::Alignment alignment, qreal offset, const QRectF subRect)
4601{
4602 Q_Q(QQuickTableView);
4603
4604 // This function will only scroll to rows that are loaded (since we
4605 // don't know the location of unloaded rows). But as an exception, to
4606 // allow moving currentIndex out of the viewport, we support scrolling
4607 // to a row that is adjacent to the loaded table. So start by checking
4608 // if we should load en extra row.
4609 if (row < topRow()) {
4610 if (row != nextVisibleEdgeIndex(edge: Qt::TopEdge, startIndex: topRow() - 1))
4611 return false;
4612 loadEdge(edge: Qt::TopEdge, incubationMode: QQmlIncubator::Synchronous);
4613 } else if (row > bottomRow()) {
4614 if (row != nextVisibleEdgeIndex(edge: Qt::BottomEdge, startIndex: bottomRow() + 1))
4615 return false;
4616 loadEdge(edge: Qt::BottomEdge, incubationMode: QQmlIncubator::Synchronous);
4617 } else if (row < topRow() || row > bottomRow()) {
4618 return false;
4619 }
4620
4621 if (!loadedRows.contains(v: row))
4622 return false;
4623
4624 const qreal newContentY = getAlignmentContentY(row, alignment, offset, subRect);
4625 if (qFuzzyCompare(p1: newContentY, p2: q->contentY()))
4626 return true;
4627
4628 if (animate) {
4629 const qreal diffY = qAbs(t: newContentY - q->contentY());
4630 const qreal duration = qBound(min: 700., val: diffY * 5, max: 1500.);
4631 positionYAnimation.setTo(newContentY);
4632 positionYAnimation.setDuration(duration);
4633 positionYAnimation.restart();
4634 } else {
4635 positionYAnimation.stop();
4636 q->setContentY(newContentY);
4637 }
4638
4639 return true;
4640}
4641
4642bool QQuickTableViewPrivate::scrollToColumn(int column, Qt::Alignment alignment, qreal offset, const QRectF subRect)
4643{
4644 Q_Q(QQuickTableView);
4645
4646 // This function will only scroll to columns that are loaded (since we
4647 // don't know the location of unloaded columns). But as an exception, to
4648 // allow moving currentIndex out of the viewport, we support scrolling
4649 // to a column that is adjacent to the loaded table. So start by checking
4650 // if we should load en extra column.
4651 if (column < leftColumn()) {
4652 if (column != nextVisibleEdgeIndex(edge: Qt::LeftEdge, startIndex: leftColumn() - 1))
4653 return false;
4654 loadEdge(edge: Qt::LeftEdge, incubationMode: QQmlIncubator::Synchronous);
4655 } else if (column > rightColumn()) {
4656 if (column != nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: rightColumn() + 1))
4657 return false;
4658 loadEdge(edge: Qt::RightEdge, incubationMode: QQmlIncubator::Synchronous);
4659 } else if (column < leftColumn() || column > rightColumn()) {
4660 return false;
4661 }
4662
4663 if (!loadedColumns.contains(v: column))
4664 return false;
4665
4666 const qreal newContentX = getAlignmentContentX(column, alignment, offset, subRect);
4667 if (qFuzzyCompare(p1: newContentX, p2: q->contentX()))
4668 return true;
4669
4670 if (animate) {
4671 const qreal diffX = qAbs(t: newContentX - q->contentX());
4672 const qreal duration = qBound(min: 700., val: diffX * 5, max: 1500.);
4673 positionXAnimation.setTo(newContentX);
4674 positionXAnimation.setDuration(duration);
4675 positionXAnimation.restart();
4676 } else {
4677 positionXAnimation.stop();
4678 q->setContentX(newContentX);
4679 }
4680
4681 return true;
4682}
4683
4684void QQuickTableViewPrivate::scheduleRebuildIfFastFlick()
4685{
4686 Q_Q(QQuickTableView);
4687 // If the viewport has moved more than one page vertically or horizontally, we switch
4688 // strategy from refilling edges around the current table to instead rebuild the table
4689 // from scratch inside the new viewport. This will greatly improve performance when flicking
4690 // a long distance in one go, which can easily happen when dragging on scrollbars.
4691 // Note that we don't want to update the content size in this case, since first of all, the
4692 // content size should logically not change as a result of flicking. But more importantly, updating
4693 // the content size in combination with fast-flicking has a tendency to cause flicker in the viewport.
4694
4695 // Check the viewport moved more than one page vertically
4696 if (!viewportRect.intersects(r: QRectF(viewportRect.x(), q->contentY(), 1, q->height()))) {
4697 scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftRow;
4698 scheduledRebuildOptions |= RebuildOption::ViewportOnly;
4699 }
4700
4701 // Check the viewport moved more than one page horizontally
4702 if (!viewportRect.intersects(r: QRectF(q->contentX(), viewportRect.y(), q->width(), 1))) {
4703 scheduledRebuildOptions |= RebuildOption::CalculateNewTopLeftColumn;
4704 scheduledRebuildOptions |= RebuildOption::ViewportOnly;
4705 }
4706}
4707
4708void QQuickTableViewPrivate::setLocalViewportX(qreal contentX)
4709{
4710 // Set the new viewport position if changed, but don't trigger any
4711 // rebuilds or updates. We use this function internally to distinguish
4712 // external flicking from internal sync-ing of the content view.
4713 Q_Q(QQuickTableView);
4714 QBoolBlocker blocker(inSetLocalViewportPos, true);
4715
4716 if (qFuzzyCompare(p1: contentX, p2: q->contentX()))
4717 return;
4718
4719 q->setContentX(contentX);
4720}
4721
4722void QQuickTableViewPrivate::setLocalViewportY(qreal contentY)
4723{
4724 // Set the new viewport position if changed, but don't trigger any
4725 // rebuilds or updates. We use this function internally to distinguish
4726 // external flicking from internal sync-ing of the content view.
4727 Q_Q(QQuickTableView);
4728 QBoolBlocker blocker(inSetLocalViewportPos, true);
4729
4730 if (qFuzzyCompare(p1: contentY, p2: q->contentY()))
4731 return;
4732
4733 q->setContentY(contentY);
4734}
4735
4736void QQuickTableViewPrivate::syncViewportRect()
4737{
4738 // Sync viewportRect so that it contains the actual geometry of the viewport.
4739 // Since the column (and row) size of a sync child is decided by the column size
4740 // of its sync view, the viewport width of a sync view needs to be the maximum of
4741 // the sync views width, and its sync childrens width. This to ensure that no sync
4742 // child loads a column which is not yet loaded by the sync view, since then the
4743 // implicit column size cannot be resolved.
4744 Q_Q(QQuickTableView);
4745
4746 qreal w = q->width();
4747 qreal h = q->height();
4748
4749 for (auto syncChild : std::as_const(t&: syncChildren)) {
4750 auto syncChild_d = syncChild->d_func();
4751 if (syncChild_d->syncHorizontally)
4752 w = qMax(a: w, b: syncChild->width());
4753 if (syncChild_d->syncHorizontally)
4754 h = qMax(a: h, b: syncChild->height());
4755 }
4756
4757 viewportRect = QRectF(q->contentX(), q->contentY(), w, h);
4758}
4759
4760void QQuickTableViewPrivate::init()
4761{
4762 Q_Q(QQuickTableView);
4763
4764 q->setFlag(flag: QQuickItem::ItemIsFocusScope);
4765 q->setActiveFocusOnTab(true);
4766
4767 positionXAnimation.setTargetObject(q);
4768 positionXAnimation.setProperty(QStringLiteral("contentX"));
4769 positionXAnimation.setEasing(QEasingCurve::OutQuart);
4770
4771 positionYAnimation.setTargetObject(q);
4772 positionYAnimation.setProperty(QStringLiteral("contentY"));
4773 positionYAnimation.setEasing(QEasingCurve::OutQuart);
4774
4775 auto tapHandler = new QQuickTableViewTapHandler(q);
4776
4777 hoverHandler = new QQuickTableViewHoverHandler(q);
4778 resizeHandler = new QQuickTableViewResizeHandler(q);
4779 hoverHandler->setEnabled(resizableRows || resizableColumns);
4780 resizeHandler->setEnabled(resizableRows || resizableColumns);
4781
4782 // To allow for a more snappy UX, we try to change the current index already upon
4783 // receiving a pointer press. But we should only do that if the view is not interactive
4784 // (so that it doesn't interfere with flicking), and if the resizeHandler is not
4785 // being hovered/dragged. For those cases, we fall back to setting the current index
4786 // on tap instead. A double tap on a resize area should also revert the section size
4787 // back to its implicit size.
4788 QObject::connect(sender: tapHandler, signal: &QQuickTapHandler::pressedChanged, slot: [this, q, tapHandler] {
4789 if (!tapHandler->isPressed())
4790 return;
4791
4792 positionXAnimation.stop();
4793 positionYAnimation.stop();
4794
4795 if (!q->isInteractive())
4796 handleTap(point: tapHandler->point());
4797 });
4798
4799 QObject::connect(sender: tapHandler, signal: &QQuickTapHandler::singleTapped, slot: [this, q, tapHandler] {
4800 if (q->isInteractive())
4801 handleTap(point: tapHandler->point());
4802 });
4803
4804 QObject::connect(sender: tapHandler, signal: &QQuickTapHandler::doubleTapped, slot: [this, q, tapHandler] {
4805 const bool resizeRow = resizableRows && hoverHandler->m_row != -1;
4806 const bool resizeColumn = resizableColumns && hoverHandler->m_column != -1;
4807
4808 if (resizeRow || resizeColumn) {
4809 if (resizeRow)
4810 q->setRowHeight(row: hoverHandler->m_row, size: -1);
4811 if (resizeColumn)
4812 q->setColumnWidth(column: hoverHandler->m_column, size: -1);
4813 } else if (editTriggers & QQuickTableView::DoubleTapped) {
4814 const QPointF pos = tapHandler->point().pressPosition();
4815 const QPoint cell = q->cellAtPosition(position: pos);
4816 const QModelIndex index = q->modelIndex(cell);
4817 if (canEdit(tappedIndex: index, warn: false))
4818 q->edit(index);
4819 }
4820 });
4821}
4822
4823void QQuickTableViewPrivate::handleTap(const QQuickHandlerPoint &point)
4824{
4825 Q_Q(QQuickTableView);
4826
4827 if (keyNavigationEnabled)
4828 q->forceActiveFocus(reason: Qt::MouseFocusReason);
4829
4830 if (point.modifiers() != Qt::NoModifier)
4831 return;
4832 if (resizableRows && hoverHandler->m_row != -1)
4833 return;
4834 if (resizableColumns && hoverHandler->m_column != -1)
4835 return;
4836 if (resizeHandler->state() != QQuickTableViewResizeHandler::Listening)
4837 return;
4838
4839 const QModelIndex tappedIndex = q->modelIndex(cell: q->cellAtPosition(position: point.position()));
4840 bool tappedCellIsSelected = false;
4841
4842 if (selectionModel)
4843 tappedCellIsSelected = selectionModel->isSelected(index: tappedIndex);
4844
4845 if (canEdit(tappedIndex, warn: false)) {
4846 if (editTriggers & QQuickTableView::SingleTapped) {
4847 if (selectionBehavior != QQuickTableView::SelectionDisabled)
4848 clearSelection();
4849 q->edit(index: tappedIndex);
4850 return;
4851 } else if (editTriggers & QQuickTableView::SelectedTapped && tappedCellIsSelected) {
4852 q->edit(index: tappedIndex);
4853 return;
4854 }
4855 }
4856
4857 // Since the tap didn't result in selecting or editing cells, we clear
4858 // the current selection and move the current index instead.
4859 if (pointerNavigationEnabled) {
4860 q->closeEditor();
4861 if (selectionBehavior != QQuickTableView::SelectionDisabled)
4862 clearSelection();
4863 setCurrentIndexFromTap(point.position());
4864 }
4865}
4866
4867bool QQuickTableViewPrivate::canEdit(const QModelIndex tappedIndex, bool warn)
4868{
4869 // Check that a call to edit(tappedIndex) would not
4870 // result in warnings being printed.
4871 Q_Q(QQuickTableView);
4872
4873 if (!tappedIndex.isValid()) {
4874 if (warn)
4875 qmlWarning(me: q) << "cannot edit: index is not valid!";
4876 return false;
4877 }
4878
4879 if (auto const qaim = model->abstractItemModel()) {
4880 if (!(qaim->flags(index: tappedIndex) & Qt::ItemIsEditable)) {
4881 if (warn)
4882 qmlWarning(me: q) << "cannot edit: QAbstractItemModel::flags(index) doesn't contain Qt::ItemIsEditable";
4883 return false;
4884 }
4885 }
4886
4887 const QPoint cell = q->cellAtIndex(index: tappedIndex);
4888 const QQuickItem *cellItem = q->itemAtCell(cell);
4889 if (!cellItem) {
4890 if (warn)
4891 qmlWarning(me: q) << "cannot edit: the cell to edit is not inside the viewport!";
4892 return false;
4893 }
4894
4895 auto attached = getAttachedObject(object: cellItem);
4896 if (!attached || !attached->editDelegate()) {
4897 if (warn)
4898 qmlWarning(me: q) << "cannot edit: no TableView.editDelegate set!";
4899 return false;
4900 }
4901
4902 return true;
4903}
4904
4905void QQuickTableViewPrivate::syncViewportPosRecursive()
4906{
4907 Q_Q(QQuickTableView);
4908 QBoolBlocker recursionGuard(inSyncViewportPosRecursive, true);
4909
4910 if (syncView) {
4911 auto syncView_d = syncView->d_func();
4912 if (!syncView_d->inSyncViewportPosRecursive) {
4913 if (syncHorizontally)
4914 syncView_d->setLocalViewportX(q->contentX());
4915 if (syncVertically)
4916 syncView_d->setLocalViewportY(q->contentY());
4917 syncView_d->syncViewportPosRecursive();
4918 }
4919 }
4920
4921 for (auto syncChild : std::as_const(t&: syncChildren)) {
4922 auto syncChild_d = syncChild->d_func();
4923 if (!syncChild_d->inSyncViewportPosRecursive) {
4924 if (syncChild_d->syncHorizontally)
4925 syncChild_d->setLocalViewportX(q->contentX());
4926 if (syncChild_d->syncVertically)
4927 syncChild_d->setLocalViewportY(q->contentY());
4928 syncChild_d->syncViewportPosRecursive();
4929 }
4930 }
4931}
4932
4933void QQuickTableViewPrivate::setCurrentIndexFromTap(const QPointF &pos)
4934{
4935 Q_Q(QQuickTableView);
4936
4937 const QPoint cell = q->cellAtPosition(position: pos);
4938 if (!cellIsValid(cell))
4939 return;
4940
4941 setCurrentIndex(cell);
4942}
4943
4944void QQuickTableViewPrivate::setCurrentIndex(const QPoint &cell)
4945{
4946 if (!selectionModel)
4947 return;
4948
4949 const auto index = q_func()->modelIndex(cell);
4950 selectionModel->setCurrentIndex(index, command: QItemSelectionModel::NoUpdate);
4951}
4952
4953bool QQuickTableViewPrivate::setCurrentIndexFromKeyEvent(QKeyEvent *e)
4954{
4955 Q_Q(QQuickTableView);
4956
4957 if (!selectionModel || !selectionModel->model())
4958 return false;
4959
4960 const QModelIndex currentIndex = selectionModel->currentIndex();
4961 const QPoint currentCell = q->cellAtIndex(index: currentIndex);
4962 const bool select = (e->modifiers() & Qt::ShiftModifier) && (e->key() != Qt::Key_Backtab);
4963
4964 if (!q->activeFocusOnTab()) {
4965 switch (e->key()) {
4966 case Qt::Key_Tab:
4967 case Qt::Key_Backtab:
4968 return false;
4969 }
4970 }
4971
4972 if (!cellIsValid(cell: currentCell)) {
4973 switch (e->key()) {
4974 case Qt::Key_Up:
4975 case Qt::Key_Down:
4976 case Qt::Key_Left:
4977 case Qt::Key_Right:
4978 case Qt::Key_PageUp:
4979 case Qt::Key_PageDown:
4980 case Qt::Key_Home:
4981 case Qt::Key_End:
4982 case Qt::Key_Tab:
4983 case Qt::Key_Backtab:
4984 // Special case: the current index doesn't map to a cell in the view (perhaps
4985 // because it isn't set yet). In that case, we set it to be the top-left cell.
4986 const QModelIndex topLeftIndex = q->index(row: topRow(), column: leftColumn());
4987 selectionModel->setCurrentIndex(index: topLeftIndex, command: QItemSelectionModel::NoUpdate);
4988 return true;
4989 }
4990 return false;
4991 }
4992
4993 auto beginMoveCurrentIndex = [&](){
4994 if (!select) {
4995 clearSelection();
4996 } else if (selectionRectangle().isEmpty()) {
4997 const int serializedStartIndex = modelIndexToCellIndex(modelIndex: selectionModel->currentIndex());
4998 if (loadedItems.contains(key: serializedStartIndex)) {
4999 const QRectF startGeometry = loadedItems.value(key: serializedStartIndex)->geometry();
5000 setSelectionStartPos(startGeometry.center());
5001 }
5002 }
5003 };
5004
5005 auto endMoveCurrentIndex = [&](const QPoint &cell){
5006 if (select) {
5007 if (polishScheduled)
5008 forceLayout(immediate: true);
5009 const int serializedEndIndex = modelIndexAtCell(cell);
5010 if (loadedItems.contains(key: serializedEndIndex)) {
5011 const QRectF endGeometry = loadedItems.value(key: serializedEndIndex)->geometry();
5012 setSelectionEndPos(endGeometry.center());
5013 }
5014 }
5015 selectionModel->setCurrentIndex(index: q->modelIndex(cell), command: QItemSelectionModel::NoUpdate);
5016 };
5017
5018 switch (e->key()) {
5019 case Qt::Key_Up: {
5020 beginMoveCurrentIndex();
5021 const int nextRow = nextVisibleEdgeIndex(edge: Qt::TopEdge, startIndex: currentCell.y() - 1);
5022 if (nextRow == kEdgeIndexAtEnd)
5023 break;
5024 const qreal marginY = atTableEnd(edge: Qt::TopEdge, startIndex: nextRow - 1) ? -q->topMargin() : 0;
5025 q->positionViewAtRow(row: nextRow, mode: QQuickTableView::Contain, offset: marginY);
5026 endMoveCurrentIndex({currentCell.x(), nextRow});
5027 break; }
5028 case Qt::Key_Down: {
5029 beginMoveCurrentIndex();
5030 const int nextRow = nextVisibleEdgeIndex(edge: Qt::BottomEdge, startIndex: currentCell.y() + 1);
5031 if (nextRow == kEdgeIndexAtEnd)
5032 break;
5033 const qreal marginY = atTableEnd(edge: Qt::BottomEdge, startIndex: nextRow + 1) ? q->bottomMargin() : 0;
5034 q->positionViewAtRow(row: nextRow, mode: QQuickTableView::Contain, offset: marginY);
5035 endMoveCurrentIndex({currentCell.x(), nextRow});
5036 break; }
5037 case Qt::Key_Left: {
5038 beginMoveCurrentIndex();
5039 const int nextColumn = nextVisibleEdgeIndex(edge: Qt::LeftEdge, startIndex: currentCell.x() - 1);
5040 if (nextColumn == kEdgeIndexAtEnd)
5041 break;
5042 const qreal marginX = atTableEnd(edge: Qt::LeftEdge, startIndex: nextColumn - 1) ? -q->leftMargin() : 0;
5043 q->positionViewAtColumn(column: nextColumn, mode: QQuickTableView::Contain, offset: marginX);
5044 endMoveCurrentIndex({nextColumn, currentCell.y()});
5045 break; }
5046 case Qt::Key_Right: {
5047 beginMoveCurrentIndex();
5048 const int nextColumn = nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: currentCell.x() + 1);
5049 if (nextColumn == kEdgeIndexAtEnd)
5050 break;
5051 const qreal marginX = atTableEnd(edge: Qt::RightEdge, startIndex: nextColumn + 1) ? q->rightMargin() : 0;
5052 q->positionViewAtColumn(column: nextColumn, mode: QQuickTableView::Contain, offset: marginX);
5053 endMoveCurrentIndex({nextColumn, currentCell.y()});
5054 break; }
5055 case Qt::Key_PageDown: {
5056 int newBottomRow = -1;
5057 beginMoveCurrentIndex();
5058 if (currentCell.y() < bottomRow()) {
5059 // The first PageDown should just move currentIndex to the bottom
5060 newBottomRow = bottomRow();
5061 q->positionViewAtRow(row: newBottomRow, mode: QQuickTableView::AlignBottom, offset: 0);
5062 } else {
5063 q->positionViewAtRow(row: bottomRow(), mode: QQuickTableView::AlignTop, offset: 0);
5064 positionYAnimation.complete();
5065 newBottomRow = topRow() != bottomRow() ? bottomRow() : bottomRow() + 1;
5066 const qreal marginY = atTableEnd(edge: Qt::BottomEdge, startIndex: newBottomRow + 1) ? q->bottomMargin() : 0;
5067 q->positionViewAtRow(row: newBottomRow, mode: QQuickTableView::AlignTop | QQuickTableView::AlignBottom, offset: marginY);
5068 positionYAnimation.complete();
5069 }
5070 endMoveCurrentIndex(QPoint(currentCell.x(), newBottomRow));
5071 break; }
5072 case Qt::Key_PageUp: {
5073 int newTopRow = -1;
5074 beginMoveCurrentIndex();
5075 if (currentCell.y() > topRow()) {
5076 // The first PageUp should just move currentIndex to the top
5077 newTopRow = topRow();
5078 q->positionViewAtRow(row: newTopRow, mode: QQuickTableView::AlignTop, offset: 0);
5079 } else {
5080 q->positionViewAtRow(row: topRow(), mode: QQuickTableView::AlignBottom, offset: 0);
5081 positionYAnimation.complete();
5082 newTopRow = topRow() != bottomRow() ? topRow() : topRow() - 1;
5083 const qreal marginY = atTableEnd(edge: Qt::TopEdge, startIndex: newTopRow - 1) ? -q->topMargin() : 0;
5084 q->positionViewAtRow(row: newTopRow, mode: QQuickTableView::AlignTop, offset: marginY);
5085 positionYAnimation.complete();
5086 }
5087 endMoveCurrentIndex(QPoint(currentCell.x(), newTopRow));
5088 break; }
5089 case Qt::Key_Home: {
5090 beginMoveCurrentIndex();
5091 const int firstColumn = nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: 0);
5092 q->positionViewAtColumn(column: firstColumn, mode: QQuickTableView::AlignLeft, offset: -q->leftMargin());
5093 endMoveCurrentIndex(QPoint(firstColumn, currentCell.y()));
5094 break; }
5095 case Qt::Key_End: {
5096 beginMoveCurrentIndex();
5097 const int lastColumn = nextVisibleEdgeIndex(edge: Qt::LeftEdge, startIndex: tableSize.width() - 1);
5098 q->positionViewAtColumn(column: lastColumn, mode: QQuickTableView::AlignRight, offset: q->rightMargin());
5099 endMoveCurrentIndex(QPoint(lastColumn, currentCell.y()));
5100 break; }
5101 case Qt::Key_Tab: {
5102 beginMoveCurrentIndex();
5103 int nextRow = currentCell.y();
5104 int nextColumn = nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: currentCell.x() + 1);
5105 if (nextColumn == kEdgeIndexAtEnd) {
5106 nextRow = nextVisibleEdgeIndex(edge: Qt::BottomEdge, startIndex: currentCell.y() + 1);
5107 if (nextRow == kEdgeIndexAtEnd)
5108 nextRow = nextVisibleEdgeIndex(edge: Qt::BottomEdge, startIndex: 0);
5109 nextColumn = nextVisibleEdgeIndex(edge: Qt::RightEdge, startIndex: 0);
5110 const qreal marginY = atTableEnd(edge: Qt::BottomEdge, startIndex: nextRow + 1) ? q->bottomMargin() : 0;
5111 q->positionViewAtRow(row: nextRow, mode: QQuickTableView::Contain, offset: marginY);
5112 }
5113
5114 qreal marginX = 0;
5115 if (atTableEnd(edge: Qt::RightEdge, startIndex: nextColumn + 1))
5116 marginX = q->leftMargin();
5117 else if (atTableEnd(edge: Qt::LeftEdge, startIndex: nextColumn - 1))
5118 marginX = -q->leftMargin();
5119
5120 q->positionViewAtColumn(column: nextColumn, mode: QQuickTableView::Contain, offset: marginX);
5121 endMoveCurrentIndex({nextColumn, nextRow});
5122 break; }
5123 case Qt::Key_Backtab: {
5124 beginMoveCurrentIndex();
5125 int nextRow = currentCell.y();
5126 int nextColumn = nextVisibleEdgeIndex(edge: Qt::LeftEdge, startIndex: currentCell.x() - 1);
5127 if (nextColumn == kEdgeIndexAtEnd) {
5128 nextRow = nextVisibleEdgeIndex(edge: Qt::TopEdge, startIndex: currentCell.y() - 1);
5129 if (nextRow == kEdgeIndexAtEnd)
5130 nextRow = nextVisibleEdgeIndex(edge: Qt::TopEdge, startIndex: tableSize.height() - 1);
5131 nextColumn = nextVisibleEdgeIndex(edge: Qt::LeftEdge, startIndex: tableSize.width() - 1);
5132 const qreal marginY = atTableEnd(edge: Qt::TopEdge, startIndex: nextRow - 1) ? -q->topMargin() : 0;
5133 q->positionViewAtRow(row: nextRow, mode: QQuickTableView::Contain, offset: marginY);
5134 }
5135
5136 qreal marginX = 0;
5137 if (atTableEnd(edge: Qt::RightEdge, startIndex: nextColumn + 1))
5138 marginX = q->leftMargin();
5139 else if (atTableEnd(edge: Qt::LeftEdge, startIndex: nextColumn - 1))
5140 marginX = -q->leftMargin();
5141
5142 q->positionViewAtColumn(column: nextColumn, mode: QQuickTableView::Contain, offset: marginX);
5143 endMoveCurrentIndex({nextColumn, nextRow});
5144 break; }
5145 default:
5146 return false;
5147 }
5148
5149 return true;
5150}
5151
5152bool QQuickTableViewPrivate::editFromKeyEvent(QKeyEvent *e)
5153{
5154 Q_Q(QQuickTableView);
5155
5156 if (editTriggers == QQuickTableView::NoEditTriggers)
5157 return false;
5158 if (!selectionModel || !selectionModel->model())
5159 return false;
5160
5161 const QModelIndex index = selectionModel->currentIndex();
5162 const QPoint cell = q->cellAtIndex(index);
5163 const QQuickItem *cellItem = q->itemAtCell(cell);
5164 if (!cellItem)
5165 return false;
5166
5167 auto attached = getAttachedObject(object: cellItem);
5168 if (!attached || !attached->editDelegate())
5169 return false;
5170
5171 bool anyKeyPressed = false;
5172 bool editKeyPressed = false;
5173
5174 switch (e->key()) {
5175 case Qt::Key_Return:
5176 case Qt::Key_Enter:
5177#ifndef Q_OS_MACOS
5178 case Qt::Key_F2:
5179#endif
5180 anyKeyPressed = true;
5181 editKeyPressed = true;
5182 break;
5183 case Qt::Key_Shift:
5184 case Qt::Key_Alt:
5185 case Qt::Key_Control:
5186 case Qt::Key_Meta:
5187 case Qt::Key_Tab:
5188 case Qt::Key_Backtab:
5189 break;
5190 default:
5191 anyKeyPressed = true;
5192 }
5193
5194 const bool anyKeyAccepted = anyKeyPressed && (editTriggers & QQuickTableView::AnyKeyPressed);
5195 const bool editKeyAccepted = editKeyPressed && (editTriggers & QQuickTableView::EditKeyPressed);
5196
5197 if (!(editKeyAccepted || anyKeyAccepted))
5198 return false;
5199
5200 if (!canEdit(tappedIndex: index, warn: false)) {
5201 // If canEdit() returns false at this point (e.g because currentIndex is not
5202 // editable), we still want to eat the key event, to keep a consistent behavior
5203 // when some cells are editable, but others not.
5204 return true;
5205 }
5206
5207 q->edit(index);
5208
5209 if (editIndex.isValid() && anyKeyAccepted && !editKeyPressed) {
5210 // Replay the key event to the focus object (which should at this point
5211 // be the edit item, or an item inside the edit item).
5212 QGuiApplication::sendEvent(receiver: QGuiApplication::focusObject(), event: e);
5213 }
5214
5215 return true;
5216}
5217
5218#if QT_CONFIG(cursor)
5219void QQuickTableViewPrivate::updateCursor()
5220{
5221 int row = resizableRows ? hoverHandler->m_row : -1;
5222 int column = resizableColumns ? hoverHandler->m_column : -1;
5223
5224 const auto resizeState = resizeHandler->state();
5225 if (resizeState == QQuickTableViewResizeHandler::DraggingStarted
5226 || resizeState == QQuickTableViewResizeHandler::Dragging) {
5227 // Don't change the cursor while resizing, even if
5228 // the pointer is not actually hovering the grid.
5229 row = resizeHandler->m_row;
5230 column = resizeHandler->m_column;
5231 }
5232
5233 if (row != -1 || column != -1) {
5234 Qt::CursorShape shape;
5235 if (row != -1 && column != -1)
5236 shape = Qt::SizeFDiagCursor;
5237 else if (row != -1)
5238 shape = Qt::SplitVCursor;
5239 else
5240 shape = Qt::SplitHCursor;
5241
5242 if (m_cursorSet)
5243 qApp->changeOverrideCursor(shape);
5244 else
5245 qApp->setOverrideCursor(shape);
5246
5247 m_cursorSet = true;
5248 } else if (m_cursorSet) {
5249 qApp->restoreOverrideCursor();
5250 m_cursorSet = false;
5251 }
5252}
5253#endif
5254
5255void QQuickTableViewPrivate::updateEditItem()
5256{
5257 Q_Q(QQuickTableView);
5258
5259 if (!editItem)
5260 return;
5261
5262 const QPoint cell = q->cellAtIndex(index: editIndex);
5263 auto cellItem = q->itemAtCell(cell);
5264 if (!cellItem) {
5265 // The delegate item that is being edited has left the viewport. But since we
5266 // added an extra reference to it when editing began, the delegate item has
5267 // not been unloaded! It's therefore still on the content item (outside the
5268 // viewport), but its position will no longer be updated until the row and column
5269 // it's a part of enters the viewport again. To avoid glitches related to the
5270 // item showing up on wrong places (e.g after resizing a column in front of it),
5271 // we move it far out of the viewport. This way it will be "hidden", but continue
5272 // to have edit focus. When the row and column that it's a part of are eventually
5273 // flicked back in again, a relayout will move it back to the correct place.
5274 editItem->parentItem()->setX(-editItem->width() - 10000);
5275 }
5276}
5277
5278QQuickTableView::QQuickTableView(QQuickItem *parent)
5279 : QQuickFlickable(*(new QQuickTableViewPrivate), parent)
5280{
5281 d_func()->init();
5282}
5283
5284QQuickTableView::QQuickTableView(QQuickTableViewPrivate &dd, QQuickItem *parent)
5285 : QQuickFlickable(dd, parent)
5286{
5287 d_func()->init();
5288}
5289
5290QQuickTableView::~QQuickTableView()
5291{
5292}
5293
5294void QQuickTableView::componentFinalized()
5295{
5296 // componentComplete() is called on us after all static values have been assigned, but
5297 // before bindings to any anchestors has been evaluated. Especially this means that
5298 // if our size is bound to the parents size, it will still be empty at that point.
5299 // And we cannot build the table without knowing our own size. We could wait until we
5300 // got the first updatePolish() callback, but at that time, any asynchronous loaders that we
5301 // might be inside have already finished loading, which means that we would load all
5302 // the delegate items synchronously instead of asynchronously. We therefore use componentFinalized
5303 // which gets called after all the bindings we rely on has been evaluated.
5304 // When receiving this call, we load the delegate items (and build the table).
5305
5306 // Now that all bindings are evaluated, and we know
5307 // our final geometery, we can build the table.
5308 Q_D(QQuickTableView);
5309 qCDebug(lcTableViewDelegateLifecycle);
5310 d->updatePolish();
5311}
5312
5313qreal QQuickTableView::minXExtent() const
5314{
5315 return QQuickFlickable::minXExtent() - d_func()->origin.x();
5316}
5317
5318qreal QQuickTableView::maxXExtent() const
5319{
5320 return QQuickFlickable::maxXExtent() - d_func()->endExtent.width();
5321}
5322
5323qreal QQuickTableView::minYExtent() const
5324{
5325 return QQuickFlickable::minYExtent() - d_func()->origin.y();
5326}
5327
5328qreal QQuickTableView::maxYExtent() const
5329{
5330 return QQuickFlickable::maxYExtent() - d_func()->endExtent.height();
5331}
5332
5333int QQuickTableView::rows() const
5334{
5335 return d_func()->tableSize.height();
5336}
5337
5338int QQuickTableView::columns() const
5339{
5340 return d_func()->tableSize.width();
5341}
5342
5343qreal QQuickTableView::rowSpacing() const
5344{
5345 return d_func()->cellSpacing.height();
5346}
5347
5348void QQuickTableView::setRowSpacing(qreal spacing)
5349{
5350 Q_D(QQuickTableView);
5351 if (qt_is_nan(d: spacing) || !qt_is_finite(d: spacing))
5352 return;
5353 if (qFuzzyCompare(p1: d->cellSpacing.height(), p2: spacing))
5354 return;
5355
5356 d->cellSpacing.setHeight(spacing);
5357 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::LayoutOnly
5358 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentHeight);
5359 emit rowSpacingChanged();
5360}
5361
5362qreal QQuickTableView::columnSpacing() const
5363{
5364 return d_func()->cellSpacing.width();
5365}
5366
5367void QQuickTableView::setColumnSpacing(qreal spacing)
5368{
5369 Q_D(QQuickTableView);
5370 if (qt_is_nan(d: spacing) || !qt_is_finite(d: spacing))
5371 return;
5372 if (qFuzzyCompare(p1: d->cellSpacing.width(), p2: spacing))
5373 return;
5374
5375 d->cellSpacing.setWidth(spacing);
5376 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::LayoutOnly
5377 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentWidth);
5378 emit columnSpacingChanged();
5379}
5380
5381QJSValue QQuickTableView::rowHeightProvider() const
5382{
5383 return d_func()->rowHeightProvider;
5384}
5385
5386void QQuickTableView::setRowHeightProvider(const QJSValue &provider)
5387{
5388 Q_D(QQuickTableView);
5389 if (provider.strictlyEquals(other: d->rowHeightProvider))
5390 return;
5391
5392 d->rowHeightProvider = provider;
5393 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly
5394 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentHeight);
5395 emit rowHeightProviderChanged();
5396}
5397
5398QJSValue QQuickTableView::columnWidthProvider() const
5399{
5400 return d_func()->columnWidthProvider;
5401}
5402
5403void QQuickTableView::setColumnWidthProvider(const QJSValue &provider)
5404{
5405 Q_D(QQuickTableView);
5406 if (provider.strictlyEquals(other: d->columnWidthProvider))
5407 return;
5408
5409 d->columnWidthProvider = provider;
5410 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly
5411 | QQuickTableViewPrivate::RebuildOption::CalculateNewContentWidth);
5412 emit columnWidthProviderChanged();
5413}
5414
5415QVariant QQuickTableView::model() const
5416{
5417 return d_func()->modelImpl();
5418}
5419
5420void QQuickTableView::setModel(const QVariant &newModel)
5421{
5422 Q_D(QQuickTableView);
5423
5424 closeEditor();
5425 d->setModelImpl(newModel);
5426
5427 if (d->selectionModel)
5428 d->selectionModel->setModel(d->qaim(modelAsVariant: newModel));
5429}
5430
5431QQmlComponent *QQuickTableView::delegate() const
5432{
5433 return d_func()->assignedDelegate;
5434}
5435
5436void QQuickTableView::setDelegate(QQmlComponent *newDelegate)
5437{
5438 Q_D(QQuickTableView);
5439 if (newDelegate == d->assignedDelegate)
5440 return;
5441
5442 d->assignedDelegate = newDelegate;
5443 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::All);
5444
5445 emit delegateChanged();
5446}
5447
5448QQuickTableView::EditTriggers QQuickTableView::editTriggers() const
5449{
5450 return d_func()->editTriggers;
5451}
5452
5453void QQuickTableView::setEditTriggers(QQuickTableView::EditTriggers editTriggers)
5454{
5455 Q_D(QQuickTableView);
5456 if (editTriggers == d->editTriggers)
5457 return;
5458
5459 d->editTriggers = editTriggers;
5460
5461 emit editTriggersChanged();
5462}
5463
5464bool QQuickTableView::reuseItems() const
5465{
5466 return bool(d_func()->reusableFlag == QQmlTableInstanceModel::Reusable);
5467}
5468
5469void QQuickTableView::setReuseItems(bool reuse)
5470{
5471 Q_D(QQuickTableView);
5472 if (reuseItems() == reuse)
5473 return;
5474
5475 d->reusableFlag = reuse ? QQmlTableInstanceModel::Reusable : QQmlTableInstanceModel::NotReusable;
5476
5477 if (!reuse && d->tableModel) {
5478 // When we're told to not reuse items, we
5479 // immediately, as documented, drain the pool.
5480 d->tableModel->drainReusableItemsPool(maxPoolTime: 0);
5481 }
5482
5483 emit reuseItemsChanged();
5484}
5485
5486void QQuickTableView::setContentWidth(qreal width)
5487{
5488 Q_D(QQuickTableView);
5489 d->explicitContentWidth = width;
5490 QQuickFlickable::setContentWidth(width);
5491}
5492
5493void QQuickTableView::setContentHeight(qreal height)
5494{
5495 Q_D(QQuickTableView);
5496 d->explicitContentHeight = height;
5497 QQuickFlickable::setContentHeight(height);
5498}
5499
5500/*!
5501 \qmlproperty TableView QtQuick::TableView::syncView
5502
5503 If this property of a TableView is set to another TableView, both the
5504 tables will synchronize with regard to flicking, column widths/row heights,
5505 and spacing according to \l syncDirection.
5506
5507 If \l syncDirection contains \l {Qt::Horizontal}{Qt.Horizontal}, current
5508 tableView's column widths, column spacing, and horizontal flicking movement
5509 synchronizes with syncView's.
5510
5511 If \l syncDirection contains \l {Qt::Vertical}{Qt.Vertical}, current
5512 tableView's row heights, row spacing, and vertical flicking movement
5513 synchronizes with syncView's.
5514
5515 \sa syncDirection
5516*/
5517QQuickTableView *QQuickTableView::syncView() const
5518{
5519 return d_func()->assignedSyncView;
5520}
5521
5522void QQuickTableView::setSyncView(QQuickTableView *view)
5523{
5524 Q_D(QQuickTableView);
5525 if (d->assignedSyncView == view)
5526 return;
5527
5528 d->assignedSyncView = view;
5529 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly);
5530
5531 emit syncViewChanged();
5532}
5533
5534/*!
5535 \qmlproperty Qt::Orientations QtQuick::TableView::syncDirection
5536
5537 If the \l syncView is set on a TableView, this property controls
5538 synchronization of flicking direction(s) for both tables. The default is \c
5539 {Qt.Horizontal | Qt.Vertical}, which means that if you flick either table
5540 in either direction, the other table is flicked the same amount in the
5541 same direction.
5542
5543 This property and \l syncView can be used to make two tableViews
5544 synchronize with each other smoothly in flicking regardless of the different
5545 overshoot/undershoot, velocity, acceleration/deceleration or rebound
5546 animation, and so on.
5547
5548 A typical use case is to make several headers flick along with the table.
5549
5550 \sa syncView
5551*/
5552Qt::Orientations QQuickTableView::syncDirection() const
5553{
5554 return d_func()->assignedSyncDirection;
5555}
5556
5557void QQuickTableView::setSyncDirection(Qt::Orientations direction)
5558{
5559 Q_D(QQuickTableView);
5560 if (d->assignedSyncDirection == direction)
5561 return;
5562
5563 d->assignedSyncDirection = direction;
5564 if (d->assignedSyncView)
5565 d->scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::ViewportOnly);
5566
5567 emit syncDirectionChanged();
5568}
5569
5570QItemSelectionModel *QQuickTableView::selectionModel() const
5571{
5572 return d_func()->selectionModel;
5573}
5574
5575void QQuickTableView::setSelectionModel(QItemSelectionModel *selectionModel)
5576{
5577 Q_D(QQuickTableView);
5578 if (d->selectionModel == selectionModel)
5579 return;
5580
5581 // Note: There is no need to rebuild the table when the selection model
5582 // changes, since selections only affect the internals of the delegate
5583 // items, and not the layout of the TableView.
5584
5585 if (d->selectionModel) {
5586 QQuickTableViewPrivate::disconnect(sender: d->selectionModel, signal: &QItemSelectionModel::selectionChanged,
5587 receiverPrivate: d, slot: &QQuickTableViewPrivate::selectionChangedInSelectionModel);
5588 QQuickTableViewPrivate::disconnect(sender: d->selectionModel, signal: &QItemSelectionModel::currentChanged,
5589 receiverPrivate: d, slot: &QQuickTableViewPrivate::currentChangedInSelectionModel);
5590 }
5591
5592 d->selectionModel = selectionModel;
5593
5594 if (d->selectionModel) {
5595 d->selectionModel->setModel(d->qaim(modelAsVariant: d->modelImpl()));
5596 QQuickTableViewPrivate::connect(sender: d->selectionModel, signal: &QItemSelectionModel::selectionChanged,
5597 receiverPrivate: d, slot: &QQuickTableViewPrivate::selectionChangedInSelectionModel);
5598 QQuickTableViewPrivate::connect(sender: d->selectionModel, signal: &QItemSelectionModel::currentChanged,
5599 receiverPrivate: d, slot: &QQuickTableViewPrivate::currentChangedInSelectionModel);
5600 }
5601
5602 d->updateSelectedOnAllDelegateItems();
5603
5604 emit selectionModelChanged();
5605}
5606
5607bool QQuickTableView::animate() const
5608{
5609 return d_func()->animate;
5610}
5611
5612void QQuickTableView::setAnimate(bool animate)
5613{
5614 Q_D(QQuickTableView);
5615 if (d->animate == animate)
5616 return;
5617
5618 d->animate = animate;
5619 if (!animate) {
5620 d->positionXAnimation.stop();
5621 d->positionYAnimation.stop();
5622 }
5623
5624 emit animateChanged();
5625}
5626
5627bool QQuickTableView::keyNavigationEnabled() const
5628{
5629 return d_func()->keyNavigationEnabled;
5630}
5631
5632void QQuickTableView::setKeyNavigationEnabled(bool enabled)
5633{
5634 Q_D(QQuickTableView);
5635 if (d->keyNavigationEnabled == enabled)
5636 return;
5637
5638 d->keyNavigationEnabled = enabled;
5639
5640 emit keyNavigationEnabledChanged();
5641}
5642
5643bool QQuickTableView::pointerNavigationEnabled() const
5644{
5645 return d_func()->pointerNavigationEnabled;
5646}
5647
5648void QQuickTableView::setPointerNavigationEnabled(bool enabled)
5649{
5650 Q_D(QQuickTableView);
5651 if (d->pointerNavigationEnabled == enabled)
5652 return;
5653
5654 d->pointerNavigationEnabled = enabled;
5655
5656 emit pointerNavigationEnabledChanged();
5657}
5658
5659int QQuickTableView::leftColumn() const
5660{
5661 Q_D(const QQuickTableView);
5662 return d->loadedItems.isEmpty() ? -1 : d_func()->leftColumn();
5663}
5664
5665int QQuickTableView::rightColumn() const
5666{
5667 Q_D(const QQuickTableView);
5668 return d->loadedItems.isEmpty() ? -1 : d_func()->rightColumn();
5669}
5670
5671int QQuickTableView::topRow() const
5672{
5673 Q_D(const QQuickTableView);
5674 return d->loadedItems.isEmpty() ? -1 : d_func()->topRow();
5675}
5676
5677int QQuickTableView::bottomRow() const
5678{
5679 Q_D(const QQuickTableView);
5680 return d->loadedItems.isEmpty() ? -1 : d_func()->bottomRow();
5681}
5682
5683int QQuickTableView::currentRow() const
5684{
5685 return d_func()->currentRow;
5686}
5687
5688int QQuickTableView::currentColumn() const
5689{
5690 return d_func()->currentColumn;
5691}
5692
5693void QQuickTableView::positionViewAtRow(int row, PositionMode mode, qreal offset, const QRectF &subRect)
5694{
5695 Q_D(QQuickTableView);
5696 if (row < 0 || row >= rows() || d->loadedRows.isEmpty())
5697 return;
5698
5699 // Note: PositionMode::Contain is from here on translated to (Qt::AlignTop | Qt::AlignBottom).
5700 // This is an internal (unsupported) combination which means "align bottom if the whole cell
5701 // fits inside the viewport, otherwise align top".
5702
5703 if (mode & (AlignTop | AlignBottom | AlignVCenter)) {
5704 mode &= AlignTop | AlignBottom | AlignVCenter;
5705 d->positionViewAtRow(row, alignment: Qt::Alignment(int(mode)), offset, subRect);
5706 } else if (mode == Contain) {
5707 if (row < topRow()) {
5708 d->positionViewAtRow(row, alignment: Qt::AlignTop, offset, subRect);
5709 } else if (row > bottomRow()) {
5710 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5711 } else if (row == topRow()) {
5712 if (!subRect.isValid()) {
5713 d->positionViewAtRow(row, alignment: Qt::AlignTop, offset, subRect);
5714 } else {
5715 const qreal subRectTop = d->loadedTableOuterRect.top() + subRect.top();
5716 const qreal subRectBottom = d->loadedTableOuterRect.top() + subRect.bottom();
5717 if (subRectTop < d->viewportRect.y())
5718 d->positionViewAtRow(row, alignment: Qt::AlignTop, offset, subRect);
5719 else if (subRectBottom > d->viewportRect.bottom())
5720 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5721 }
5722 } else if (row == bottomRow()) {
5723 if (!subRect.isValid()) {
5724 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5725 } else {
5726 // Note: entering here means that topRow() != bottomRow(). So at least two rows are
5727 // visible in the viewport, which means that the top side of the subRect is visible.
5728 const qreal subRectBottom = d->loadedTableInnerRect.bottom() + subRect.bottom();
5729 if (subRectBottom > d->viewportRect.bottom())
5730 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5731 }
5732 }
5733 } else if (mode == Visible) {
5734 if (row < topRow()) {
5735 d->positionViewAtRow(row, alignment: Qt::AlignTop, offset: -offset, subRect);
5736 } else if (row > bottomRow()) {
5737 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5738 } else if (subRect.isValid()) {
5739 if (row == topRow()) {
5740 const qreal subRectTop = d->loadedTableOuterRect.top() + subRect.top();
5741 const qreal subRectBottom = d->loadedTableOuterRect.top() + subRect.bottom();
5742 if (subRectBottom < d->viewportRect.top())
5743 d->positionViewAtRow(row, alignment: Qt::AlignTop, offset, subRect);
5744 else if (subRectTop > d->viewportRect.bottom())
5745 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5746 } else if (row == bottomRow()) {
5747 // Note: entering here means that topRow() != bottomRow(). So at least two rows are
5748 // visible in the viewport, which means that the top side of the subRect is visible.
5749 const qreal subRectTop = d->loadedTableInnerRect.bottom() + subRect.top();
5750 if (subRectTop > d->viewportRect.bottom())
5751 d->positionViewAtRow(row, alignment: Qt::AlignTop | Qt::AlignBottom, offset, subRect);
5752 }
5753 }
5754 } else {
5755 qmlWarning(me: this) << "Unsupported mode:" << int(mode);
5756 }
5757}
5758
5759void QQuickTableView::positionViewAtColumn(int column, PositionMode mode, qreal offset, const QRectF &subRect)
5760{
5761 Q_D(QQuickTableView);
5762 if (column < 0 || column >= columns() || d->loadedColumns.isEmpty())
5763 return;
5764
5765 // Note: PositionMode::Contain is from here on translated to (Qt::AlignLeft | Qt::AlignRight).
5766 // This is an internal (unsupported) combination which means "align right if the whole cell
5767 // fits inside the viewport, otherwise align left".
5768
5769 if (mode & (AlignLeft | AlignRight | AlignHCenter)) {
5770 mode &= AlignLeft | AlignRight | AlignHCenter;
5771 d->positionViewAtColumn(column, alignment: Qt::Alignment(int(mode)), offset, subRect);
5772 } else if (mode == Contain) {
5773 if (column < leftColumn()) {
5774 d->positionViewAtColumn(column, alignment: Qt::AlignLeft, offset, subRect);
5775 } else if (column > rightColumn()) {
5776 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5777 } else if (column == leftColumn()) {
5778 if (!subRect.isValid()) {
5779 d->positionViewAtColumn(column, alignment: Qt::AlignLeft, offset, subRect);
5780 } else {
5781 const qreal subRectLeft = d->loadedTableOuterRect.left() + subRect.left();
5782 const qreal subRectRight = d->loadedTableOuterRect.left() + subRect.right();
5783 if (subRectLeft < d->viewportRect.left())
5784 d->positionViewAtColumn(column, alignment: Qt::AlignLeft, offset, subRect);
5785 else if (subRectRight > d->viewportRect.right())
5786 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5787 }
5788 } else if (column == rightColumn()) {
5789 if (!subRect.isValid()) {
5790 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5791 } else {
5792 // Note: entering here means that leftColumn() != rightColumn(). So at least two columns
5793 // are visible in the viewport, which means that the left side of the subRect is visible.
5794 const qreal subRectRight = d->loadedTableInnerRect.right() + subRect.right();
5795 if (subRectRight > d->viewportRect.right())
5796 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5797 }
5798 }
5799 } else if (mode == Visible) {
5800 if (column < leftColumn()) {
5801 d->positionViewAtColumn(column, alignment: Qt::AlignLeft, offset: -offset, subRect);
5802 } else if (column > rightColumn()) {
5803 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5804 } else if (subRect.isValid()) {
5805 if (column == leftColumn()) {
5806 const qreal subRectLeft = d->loadedTableOuterRect.left() + subRect.left();
5807 const qreal subRectRight = d->loadedTableOuterRect.left() + subRect.right();
5808 if (subRectRight < d->viewportRect.left())
5809 d->positionViewAtColumn(column, alignment: Qt::AlignLeft, offset, subRect);
5810 else if (subRectLeft > d->viewportRect.right())
5811 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5812 } else if (column == rightColumn()) {
5813 // Note: entering here means that leftColumn() != rightColumn(). So at least two columns
5814 // are visible in the viewport, which means that the left side of the subRect is visible.
5815 const qreal subRectLeft = d->loadedTableInnerRect.right() + subRect.left();
5816 if (subRectLeft > d->viewportRect.right())
5817 d->positionViewAtColumn(column, alignment: Qt::AlignLeft | Qt::AlignRight, offset, subRect);
5818 }
5819 }
5820 } else {
5821 qmlWarning(me: this) << "Unsupported mode:" << int(mode);
5822 }
5823}
5824
5825void QQuickTableView::positionViewAtCell(const QPoint &cell, PositionMode mode, const QPointF &offset, const QRectF &subRect)
5826{
5827 PositionMode horizontalMode = mode & ~(AlignTop | AlignBottom | AlignVCenter);
5828 PositionMode verticalMode = mode & ~(AlignLeft | AlignRight | AlignHCenter);
5829 if (!horizontalMode && !verticalMode) {
5830 qmlWarning(me: this) << "Unsupported mode:" << int(mode);
5831 return;
5832 }
5833
5834 if (horizontalMode)
5835 positionViewAtColumn(column: cell.x(), mode: horizontalMode, offset: offset.x(), subRect);
5836 if (verticalMode)
5837 positionViewAtRow(row: cell.y(), mode: verticalMode, offset: offset.y(), subRect);
5838}
5839
5840void QQuickTableView::positionViewAtIndex(const QModelIndex &index, PositionMode mode, const QPointF &offset, const QRectF &subRect)
5841{
5842 PositionMode horizontalMode = mode & ~(AlignTop | AlignBottom | AlignVCenter);
5843 PositionMode verticalMode = mode & ~(AlignLeft | AlignRight | AlignHCenter);
5844 if (!horizontalMode && !verticalMode) {
5845 qmlWarning(me: this) << "Unsupported mode:" << int(mode);
5846 return;
5847 }
5848
5849 if (horizontalMode)
5850 positionViewAtColumn(column: columnAtIndex(index), mode: horizontalMode, offset: offset.x(), subRect);
5851 if (verticalMode)
5852 positionViewAtRow(row: rowAtIndex(index), mode: verticalMode, offset: offset.y(), subRect);
5853}
5854
5855#if QT_DEPRECATED_SINCE(6, 5)
5856void QQuickTableView::positionViewAtCell(int column, int row, PositionMode mode, const QPointF &offset, const QRectF &subRect)
5857{
5858 PositionMode horizontalMode = mode & ~(AlignTop | AlignBottom | AlignVCenter);
5859 PositionMode verticalMode = mode & ~(AlignLeft | AlignRight | AlignHCenter);
5860 if (!horizontalMode && !verticalMode) {
5861 qmlWarning(me: this) << "Unsupported mode:" << int(mode);
5862 return;
5863 }
5864
5865 if (horizontalMode)
5866 positionViewAtColumn(column, mode: horizontalMode, offset: offset.x(), subRect);
5867 if (verticalMode)
5868 positionViewAtRow(row, mode: verticalMode, offset: offset.y(), subRect);
5869}
5870#endif
5871
5872QQuickItem *QQuickTableView::itemAtCell(const QPoint &cell) const
5873{
5874 Q_D(const QQuickTableView);
5875 const int modelIndex = d->modelIndexAtCell(cell);
5876 if (!d->loadedItems.contains(key: modelIndex))
5877 return nullptr;
5878 return d->loadedItems.value(key: modelIndex)->item;
5879}
5880
5881#if QT_DEPRECATED_SINCE(6, 5)
5882QQuickItem *QQuickTableView::itemAtCell(int column, int row) const
5883{
5884 return itemAtCell(cell: QPoint(column, row));
5885}
5886#endif
5887
5888QQuickItem *QQuickTableView::itemAtIndex(const QModelIndex &index) const
5889{
5890 Q_D(const QQuickTableView);
5891 const int serializedIndex = d->modelIndexToCellIndex(modelIndex: index);
5892 if (!d->loadedItems.contains(key: serializedIndex))
5893 return nullptr;
5894 return d->loadedItems.value(key: serializedIndex)->item;
5895}
5896
5897#if QT_DEPRECATED_SINCE(6, 4)
5898QPoint QQuickTableView::cellAtPos(qreal x, qreal y, bool includeSpacing) const
5899{
5900 return cellAtPosition(position: mapToItem(item: contentItem(), point: {x, y}), includeSpacing);
5901}
5902
5903QPoint QQuickTableView::cellAtPos(const QPointF &position, bool includeSpacing) const
5904{
5905 return cellAtPosition(position: mapToItem(item: contentItem(), point: position), includeSpacing);
5906}
5907#endif
5908
5909QPoint QQuickTableView::cellAtPosition(qreal x, qreal y, bool includeSpacing) const
5910{
5911 return cellAtPosition(position: QPoint(x, y), includeSpacing);
5912}
5913
5914QPoint QQuickTableView::cellAtPosition(const QPointF &position, bool includeSpacing) const
5915{
5916 Q_D(const QQuickTableView);
5917
5918 if (!d->loadedTableOuterRect.contains(p: position))
5919 return QPoint(-1, -1);
5920
5921 const qreal hSpace = d->cellSpacing.width();
5922 const qreal vSpace = d->cellSpacing.height();
5923 qreal currentColumnEnd = d->loadedTableOuterRect.x();
5924 qreal currentRowEnd = d->loadedTableOuterRect.y();
5925
5926 int foundColumn = -1;
5927 int foundRow = -1;
5928
5929 for (const int column : d->loadedColumns) {
5930 currentColumnEnd += d->getEffectiveColumnWidth(column);
5931 if (position.x() < currentColumnEnd) {
5932 foundColumn = column;
5933 break;
5934 }
5935 currentColumnEnd += hSpace;
5936 if (!includeSpacing && position.x() < currentColumnEnd) {
5937 // Hit spacing
5938 return QPoint(-1, -1);
5939 } else if (includeSpacing && position.x() < currentColumnEnd - (hSpace / 2)) {
5940 foundColumn = column;
5941 break;
5942 }
5943 }
5944
5945 for (const int row : d->loadedRows) {
5946 currentRowEnd += d->getEffectiveRowHeight(row);
5947 if (position.y() < currentRowEnd) {
5948 foundRow = row;
5949 break;
5950 }
5951 currentRowEnd += vSpace;
5952 if (!includeSpacing && position.y() < currentRowEnd) {
5953 // Hit spacing
5954 return QPoint(-1, -1);
5955 }
5956 if (includeSpacing && position.y() < currentRowEnd - (vSpace / 2)) {
5957 foundRow = row;
5958 break;
5959 }
5960 }
5961
5962 return QPoint(foundColumn, foundRow);
5963}
5964
5965bool QQuickTableView::isColumnLoaded(int column) const
5966{
5967 Q_D(const QQuickTableView);
5968 if (!d->loadedColumns.contains(v: column))
5969 return false;
5970
5971 if (d->rebuildState != QQuickTableViewPrivate::RebuildState::Done) {
5972 // TableView is rebuilding, and none of the rows and columns
5973 // are completely loaded until we reach the layout phase.
5974 if (d->rebuildState < QQuickTableViewPrivate::RebuildState::LayoutTable)
5975 return false;
5976 }
5977
5978 return true;
5979}
5980
5981bool QQuickTableView::isRowLoaded(int row) const
5982{
5983 Q_D(const QQuickTableView);
5984 if (!d->loadedRows.contains(v: row))
5985 return false;
5986
5987 if (d->rebuildState != QQuickTableViewPrivate::RebuildState::Done) {
5988 // TableView is rebuilding, and none of the rows and columns
5989 // are completely loaded until we reach the layout phase.
5990 if (d->rebuildState < QQuickTableViewPrivate::RebuildState::LayoutTable)
5991 return false;
5992 }
5993
5994 return true;
5995}
5996
5997qreal QQuickTableView::columnWidth(int column) const
5998{
5999 Q_D(const QQuickTableView);
6000 if (!isColumnLoaded(column))
6001 return -1;
6002
6003 return d->getEffectiveColumnWidth(column);
6004}
6005
6006qreal QQuickTableView::rowHeight(int row) const
6007{
6008 Q_D(const QQuickTableView);
6009 if (!isRowLoaded(row))
6010 return -1;
6011
6012 return d->getEffectiveRowHeight(row);
6013}
6014
6015qreal QQuickTableView::implicitColumnWidth(int column) const
6016{
6017 Q_D(const QQuickTableView);
6018 if (!isColumnLoaded(column))
6019 return -1;
6020
6021 return d->sizeHintForColumn(column);
6022}
6023
6024qreal QQuickTableView::implicitRowHeight(int row) const
6025{
6026 Q_D(const QQuickTableView);
6027 if (!isRowLoaded(row))
6028 return -1;
6029
6030 return d->sizeHintForRow(row);
6031}
6032
6033void QQuickTableView::setColumnWidth(int column, qreal size)
6034{
6035 Q_D(QQuickTableView);
6036 if (column < 0) {
6037 qmlWarning(me: this) << "column must be greather than, or equal to, zero";
6038 return;
6039 }
6040
6041 if (d->syncHorizontally) {
6042 d->syncView->setColumnWidth(column, size);
6043 return;
6044 }
6045
6046 if (qFuzzyCompare(p1: explicitColumnWidth(column), p2: size))
6047 return;
6048
6049 if (size < 0)
6050 d->explicitColumnWidths.remove(key: column);
6051 else
6052 d->explicitColumnWidths.insert(key: column, value: size);
6053
6054 if (d->loadedItems.isEmpty())
6055 return;
6056
6057 const bool allColumnsLoaded = d->atTableEnd(edge: Qt::LeftEdge) && d->atTableEnd(edge: Qt::RightEdge);
6058 if (column >= leftColumn() || column <= rightColumn() || allColumnsLoaded)
6059 d->forceLayout(immediate: false);
6060}
6061
6062void QQuickTableView::clearColumnWidths()
6063{
6064 Q_D(QQuickTableView);
6065
6066 if (d->syncHorizontally) {
6067 d->syncView->clearColumnWidths();
6068 return;
6069 }
6070
6071 if (d->explicitColumnWidths.isEmpty())
6072 return;
6073
6074 d->explicitColumnWidths.clear();
6075 d->forceLayout(immediate: false);
6076}
6077
6078qreal QQuickTableView::explicitColumnWidth(int column) const
6079{
6080 Q_D(const QQuickTableView);
6081
6082 if (d->syncHorizontally)
6083 return d->syncView->explicitColumnWidth(column);
6084
6085 const auto it = d->explicitColumnWidths.constFind(key: column);
6086 if (it != d->explicitColumnWidths.constEnd())
6087 return *it;
6088 return -1;
6089}
6090
6091void QQuickTableView::setRowHeight(int row, qreal size)
6092{
6093 Q_D(QQuickTableView);
6094 if (row < 0) {
6095 qmlWarning(me: this) << "row must be greather than, or equal to, zero";
6096 return;
6097 }
6098
6099 if (d->syncVertically) {
6100 d->syncView->setRowHeight(row, size);
6101 return;
6102 }
6103
6104 if (qFuzzyCompare(p1: explicitRowHeight(row), p2: size))
6105 return;
6106
6107 if (size < 0)
6108 d->explicitRowHeights.remove(key: row);
6109 else
6110 d->explicitRowHeights.insert(key: row, value: size);
6111
6112 if (d->loadedItems.isEmpty())
6113 return;
6114
6115 const bool allRowsLoaded = d->atTableEnd(edge: Qt::TopEdge) && d->atTableEnd(edge: Qt::BottomEdge);
6116 if (row >= topRow() || row <= bottomRow() || allRowsLoaded)
6117 d->forceLayout(immediate: false);
6118}
6119
6120void QQuickTableView::clearRowHeights()
6121{
6122 Q_D(QQuickTableView);
6123
6124 if (d->syncVertically) {
6125 d->syncView->clearRowHeights();
6126 return;
6127 }
6128
6129 if (d->explicitRowHeights.isEmpty())
6130 return;
6131
6132 d->explicitRowHeights.clear();
6133 d->forceLayout(immediate: false);
6134}
6135
6136qreal QQuickTableView::explicitRowHeight(int row) const
6137{
6138 Q_D(const QQuickTableView);
6139
6140 if (d->syncVertically)
6141 return d->syncView->explicitRowHeight(row);
6142
6143 const auto it = d->explicitRowHeights.constFind(key: row);
6144 if (it != d->explicitRowHeights.constEnd())
6145 return *it;
6146 return -1;
6147}
6148
6149QModelIndex QQuickTableView::modelIndex(const QPoint &cell) const
6150{
6151 Q_D(const QQuickTableView);
6152 if (cell.x() < 0 || cell.x() >= columns() || cell.y() < 0 || cell.y() >= rows())
6153 return {};
6154
6155 auto const qaim = d->model->abstractItemModel();
6156 if (!qaim)
6157 return {};
6158
6159 return qaim->index(row: cell.y(), column: cell.x());
6160}
6161
6162QPoint QQuickTableView::cellAtIndex(const QModelIndex &index) const
6163{
6164 if (!index.isValid() || index.parent().isValid())
6165 return {-1, -1};
6166 return {index.column(), index.row()};
6167}
6168
6169#if QT_DEPRECATED_SINCE(6, 4)
6170QModelIndex QQuickTableView::modelIndex(int row, int column) const
6171{
6172 static bool compat6_4 = qEnvironmentVariable(varName: "QT_QUICK_TABLEVIEW_COMPAT_VERSION") == QStringLiteral("6.4");
6173 if (compat6_4) {
6174 // In Qt 6.4.0 and 6.4.1, a source incompatible change led to row and column
6175 // being documented to be specified in the opposite order.
6176 // QT_QUICK_TABLEVIEW_COMPAT_VERSION can therefore be set to force tableview
6177 // to continue accepting calls to modelIndex(column, row).
6178 return modelIndex(cell: {row, column});
6179 } else {
6180 qmlWarning(me: this) << "modelIndex(row, column) is deprecated. "
6181 "Use index(row, column) instead. For more information, see "
6182 "https://doc.qt.io/qt-6/qml-qtquick-tableview-obsolete.html";
6183 return modelIndex(cell: {column, row});
6184 }
6185}
6186#endif
6187
6188QModelIndex QQuickTableView::index(int row, int column) const
6189{
6190 return modelIndex(cell: {column, row});
6191}
6192
6193int QQuickTableView::rowAtIndex(const QModelIndex &index) const
6194{
6195 return cellAtIndex(index).y();
6196}
6197
6198int QQuickTableView::columnAtIndex(const QModelIndex &index) const
6199{
6200 return cellAtIndex(index).x();
6201}
6202
6203void QQuickTableView::forceLayout()
6204{
6205 d_func()->forceLayout(immediate: true);
6206}
6207
6208void QQuickTableView::edit(const QModelIndex &index)
6209{
6210 Q_D(QQuickTableView);
6211
6212 if (!d->canEdit(tappedIndex: index, warn: true))
6213 return;
6214
6215 if (d->editIndex == index)
6216 return;
6217
6218 if (!d->tableModel)
6219 return;
6220
6221 if (!d->editModel) {
6222 d->editModel = new QQmlTableInstanceModel(qmlContext(this));
6223 d->editModel->useImportVersion(version: d->resolveImportVersion());
6224 QObject::connect(sender: d->editModel, signal: &QQmlInstanceModel::initItem,
6225 slot: [this, d] (int serializedModelIndex, QObject *object) {
6226 // initItemCallback will call setRequiredProperty for each required property in the
6227 // delegate, both for this class, but also also for any subclasses. setRequiredProperty
6228 // is currently dependent of the QQmlTableInstanceModel that was used to create the object
6229 // in order to initialize required properties, so we need to set the editItem variable
6230 // early on, so that we can use it in setRequiredProperty.
6231 d->editIndex = modelIndex(cell: d->cellAtModelIndex(modelIndex: serializedModelIndex));
6232 d->editItem = qmlobject_cast<QQuickItem*>(object);
6233 if (!d->editItem)
6234 return;
6235 // Initialize required properties
6236 d->initItemCallback(modelIndex: serializedModelIndex, object);
6237 const auto cellItem = itemAtCell(cell: cellAtIndex(index: d->editIndex));
6238 Q_ASSERT(cellItem);
6239 d->editItem->setParentItem(cellItem);
6240 // Move the cell item to the top of the other items, to ensure
6241 // that e.g a focus frame ends up on top of all the cells
6242 cellItem->setZ(2);
6243 });
6244 }
6245
6246 if (d->selectionModel)
6247 d->selectionModel->setCurrentIndex(index, command: QItemSelectionModel::NoUpdate);
6248
6249 if (d->editIndex.isValid())
6250 closeEditor();
6251
6252 const auto cellItem = itemAtCell(cell: cellAtIndex(index));
6253 Q_ASSERT(cellItem);
6254 const auto attached = d->getAttachedObject(object: cellItem);
6255 Q_ASSERT(attached);
6256
6257 d->editModel->setModel(d->tableModel->model());
6258 d->editModel->setDelegate(attached->editDelegate());
6259
6260 const int cellIndex = d->modelIndexToCellIndex(modelIndex: index);
6261 QObject* object = d->editModel->object(index: cellIndex, incubationMode: QQmlIncubator::Synchronous);
6262 if (!object) {
6263 d->editIndex = QModelIndex();
6264 d->editItem = nullptr;
6265 qmlWarning(me: this) << "cannot edit: TableView.editDelegate could not be instantiated!";
6266 return;
6267 }
6268
6269 // Note: at this point, editIndex and editItem has been set from initItem!
6270
6271 if (!d->editItem) {
6272 qmlWarning(me: this) << "cannot edit: TableView.editDelegate is not an Item!";
6273 d->editItem = nullptr;
6274 d->editIndex = QModelIndex();
6275 d->editModel->release(object, reusable: QQmlInstanceModel::NotReusable);
6276 return;
6277 }
6278
6279 // Reference the cell item once more, so that it doesn't
6280 // get reused or deleted if it leaves the viewport.
6281 d->model->object(index: cellIndex, incubationMode: QQmlIncubator::Synchronous);
6282
6283 // Inform the delegate, and the edit delegate, that they're being edited
6284 d->setRequiredProperty(property: kRequiredProperty_editing, value: QVariant::fromValue(value: true), serializedModelIndex: cellIndex, object: cellItem, init: false);
6285
6286 // Transfer focus to the edit item
6287 d->editItem->forceActiveFocus(reason: Qt::MouseFocusReason);
6288
6289 // Install an event filter on the focus object to handle Enter and Tab.
6290 // Note that the focusObject doesn't need to be the editItem itself, in
6291 // case the editItem is a FocusScope.
6292 if (QObject *focusObject = d->editItem->window()->focusObject()) {
6293 QQuickItem *focusItem = qobject_cast<QQuickItem *>(o: focusObject);
6294 if (focusItem == d->editItem || d->editItem->isAncestorOf(child: focusItem))
6295 focusItem->installEventFilter(filterObj: this);
6296 }
6297}
6298
6299void QQuickTableView::closeEditor()
6300{
6301 Q_D(QQuickTableView);
6302
6303 if (!d->editItem)
6304 return;
6305
6306 QQuickItem *cellItem = d->editItem->parentItem();
6307 d->editModel->release(object: d->editItem, reusable: QQmlInstanceModel::NotReusable);
6308 d->editItem = nullptr;
6309
6310 cellItem->setZ(1);
6311 const int cellIndex = d->modelIndexToCellIndex(modelIndex: d->editIndex);
6312 d->setRequiredProperty(property: kRequiredProperty_editing, value: QVariant::fromValue(value: false), serializedModelIndex: cellIndex, object: cellItem, init: false);
6313 // Remove the extra reference we sat on the cell item from edit()
6314 d->model->release(object: cellItem, reusableFlag: QQmlInstanceModel::NotReusable);
6315
6316 if (d->editIndex.isValid()) {
6317 // Note: we can have an invalid editIndex, even when we
6318 // have an editItem, if the model has changed (e.g been reset)!
6319 d->editIndex = QModelIndex();
6320 }
6321}
6322
6323QQuickTableViewAttached *QQuickTableView::qmlAttachedProperties(QObject *obj)
6324{
6325 return new QQuickTableViewAttached(obj);
6326}
6327
6328void QQuickTableView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
6329{
6330 Q_D(QQuickTableView);
6331 QQuickFlickable::geometryChange(newGeometry, oldGeometry);
6332
6333 if (d->tableModel) {
6334 // When the view changes size, we force the pool to
6335 // shrink by releasing all pooled items.
6336 d->tableModel->drainReusableItemsPool(maxPoolTime: 0);
6337 }
6338
6339 d->forceLayout(immediate: false);
6340}
6341
6342void QQuickTableView::viewportMoved(Qt::Orientations orientation)
6343{
6344 Q_D(QQuickTableView);
6345
6346 // If the new viewport position was set from the setLocalViewportXY()
6347 // functions, we just update the position silently and return. Otherwise, if
6348 // the viewport was flicked by the user, or some other control, we
6349 // recursively sync all the views in the hierarchy to the same position.
6350 QQuickFlickable::viewportMoved(orient: orientation);
6351 if (d->inSetLocalViewportPos)
6352 return;
6353
6354 // Move all views in the syncView hierarchy to the same contentX/Y.
6355 // We need to start from this view (and not the root syncView) to
6356 // ensure that we respect all the individual syncDirection flags
6357 // between the individual views in the hierarchy.
6358 d->syncViewportPosRecursive();
6359
6360 auto rootView = d->rootSyncView();
6361 auto rootView_d = rootView->d_func();
6362
6363 rootView_d->scheduleRebuildIfFastFlick();
6364
6365 if (!rootView_d->polishScheduled) {
6366 if (rootView_d->scheduledRebuildOptions) {
6367 // When we need to rebuild, collecting several viewport
6368 // moves and do a single polish gives a quicker UI.
6369 rootView->polish();
6370 } else {
6371 // Updating the table right away when flicking
6372 // slowly gives a smoother experience.
6373 const bool updated = rootView->d_func()->updateTableRecursive();
6374 if (!updated) {
6375 // One, or more, of the views are already in an
6376 // update, so we need to wait a cycle.
6377 rootView->polish();
6378 }
6379 }
6380 }
6381}
6382
6383void QQuickTableView::keyPressEvent(QKeyEvent *e)
6384{
6385 Q_D(QQuickTableView);
6386
6387 if (!d->keyNavigationEnabled) {
6388 QQuickFlickable::keyPressEvent(event: e);
6389 return;
6390 }
6391
6392 if (d->tableSize.isEmpty())
6393 return;
6394
6395 if (d->editIndex.isValid()) {
6396 // While editing, we limit the keys that we
6397 // handle to not interfere with editing.
6398 return;
6399 }
6400
6401 if (d->setCurrentIndexFromKeyEvent(e))
6402 return;
6403
6404 if (d->editFromKeyEvent(e))
6405 return;
6406
6407 QQuickFlickable::keyPressEvent(event: e);
6408}
6409
6410bool QQuickTableView::eventFilter(QObject *obj, QEvent *event)
6411{
6412 Q_D(QQuickTableView);
6413
6414 if (event->type() == QEvent::KeyPress) {
6415 Q_ASSERT(d->editItem);
6416 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
6417 switch (keyEvent->key()) {
6418 case Qt::Key_Enter:
6419 case Qt::Key_Return:
6420 if (auto attached = d->getAttachedObject(object: d->editItem))
6421 emit attached->commit();
6422 closeEditor();
6423 return true;
6424 case Qt::Key_Tab:
6425 case Qt::Key_Backtab:
6426 if (activeFocusOnTab()) {
6427 if (auto attached = d->getAttachedObject(object: d->editItem))
6428 emit attached->commit();
6429 closeEditor();
6430 if (d->setCurrentIndexFromKeyEvent(keyEvent)) {
6431 const QModelIndex currentIndex = d->selectionModel->currentIndex();
6432 if (d->canEdit(tappedIndex: currentIndex, warn: false))
6433 edit(index: currentIndex);
6434 }
6435 return true;
6436 }
6437 break;
6438 case Qt::Key_Escape:
6439 closeEditor();
6440 return true;
6441 }
6442 }
6443
6444 return QQuickFlickable::eventFilter(watched: obj, event);
6445}
6446
6447bool QQuickTableView::alternatingRows() const
6448{
6449 return d_func()->alternatingRows;
6450}
6451
6452void QQuickTableView::setAlternatingRows(bool alternatingRows)
6453{
6454 Q_D(QQuickTableView);
6455 if (d->alternatingRows == alternatingRows)
6456 return;
6457
6458 d->alternatingRows = alternatingRows;
6459 emit alternatingRowsChanged();
6460}
6461
6462QQuickTableView::SelectionBehavior QQuickTableView::selectionBehavior() const
6463{
6464 return d_func()->selectionBehavior;
6465}
6466
6467void QQuickTableView::setSelectionBehavior(SelectionBehavior selectionBehavior)
6468{
6469 Q_D(QQuickTableView);
6470 if (d->selectionBehavior == selectionBehavior)
6471 return;
6472
6473 d->selectionBehavior = selectionBehavior;
6474 emit selectionBehaviorChanged();
6475}
6476
6477QQuickTableView::SelectionMode QQuickTableView::selectionMode() const
6478{
6479 return d_func()->selectionMode;
6480}
6481
6482void QQuickTableView::setSelectionMode(SelectionMode selectionMode)
6483{
6484 Q_D(QQuickTableView);
6485 if (d->selectionMode == selectionMode)
6486 return;
6487
6488 d->selectionMode = selectionMode;
6489 emit selectionModeChanged();
6490}
6491
6492bool QQuickTableView::resizableColumns() const
6493{
6494 return d_func()->resizableColumns;
6495}
6496
6497void QQuickTableView::setResizableColumns(bool enabled)
6498{
6499 Q_D(QQuickTableView);
6500 if (d->resizableColumns == enabled)
6501 return;
6502
6503 d->resizableColumns = enabled;
6504 d->resizeHandler->setEnabled(d->resizableRows || d->resizableColumns);
6505 d->hoverHandler->setEnabled(d->resizableRows || d->resizableColumns);
6506
6507 emit resizableColumnsChanged();
6508}
6509
6510bool QQuickTableView::resizableRows() const
6511{
6512 return d_func()->resizableRows;
6513}
6514
6515void QQuickTableView::setResizableRows(bool enabled)
6516{
6517 Q_D(QQuickTableView);
6518 if (d->resizableRows == enabled)
6519 return;
6520
6521 d->resizableRows = enabled;
6522 d->resizeHandler->setEnabled(d->resizableRows || d->resizableColumns);
6523 d->hoverHandler->setEnabled(d->resizableRows || d->resizableColumns);
6524
6525 emit resizableRowsChanged();
6526}
6527
6528// ----------------------------------------------
6529
6530QQuickTableViewHoverHandler::QQuickTableViewHoverHandler(QQuickTableView *view)
6531 : QQuickHoverHandler(view->contentItem())
6532{
6533 setMargin(5);
6534
6535 connect(sender: this, signal: &QQuickHoverHandler::hoveredChanged, slot: [this] {
6536 if (!isHoveringGrid())
6537 return;
6538 m_row = -1;
6539 m_column = -1;
6540#if QT_CONFIG(cursor)
6541 auto tableView = static_cast<QQuickTableView *>(parentItem()->parent());
6542 auto tableViewPrivate = QQuickTableViewPrivate::get(q: tableView);
6543 tableViewPrivate->updateCursor();
6544#endif
6545 });
6546}
6547
6548void QQuickTableViewHoverHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point)
6549{
6550 QQuickHoverHandler::handleEventPoint(ev: event, point);
6551
6552 auto tableView = static_cast<QQuickTableView *>(parentItem()->parent());
6553#if QT_CONFIG(cursor)
6554 auto tableViewPrivate = QQuickTableViewPrivate::get(q: tableView);
6555#endif
6556
6557 const QPoint cell = tableView->cellAtPosition(position: point.position(), includeSpacing: true);
6558 const auto item = tableView->itemAtCell(cell);
6559 if (!item) {
6560 m_row = -1;
6561 m_column = -1;
6562#if QT_CONFIG(cursor)
6563 tableViewPrivate->updateCursor();
6564#endif
6565 return;
6566 }
6567
6568 const QPointF itemPos = item->mapFromItem(item: tableView->contentItem(), point: point.position());
6569 const bool hoveringRow = (itemPos.y() < margin() || itemPos.y() > item->height() - margin());
6570 const bool hoveringColumn = (itemPos.x() < margin() || itemPos.x() > item->width() - margin());
6571 m_row = hoveringRow ? itemPos.y() < margin() ? cell.y() - 1 : cell.y() : -1;
6572 m_column = hoveringColumn ? itemPos.x() < margin() ? cell.x() - 1 : cell.x() : -1;
6573#if QT_CONFIG(cursor)
6574 tableViewPrivate->updateCursor();
6575#endif
6576}
6577
6578// ----------------------------------------------
6579
6580QQuickTableViewResizeHandler::QQuickTableViewResizeHandler(QQuickTableView *view)
6581 : QQuickSinglePointHandler(view->contentItem())
6582{
6583 setMargin(5);
6584 // Set a grab permission that stops the flickable, as well as
6585 // any drag handler inside the delegate, from stealing the drag.
6586 setGrabPermissions(QQuickPointerHandler::CanTakeOverFromAnything);
6587 setObjectName("tableViewResizeHandler");
6588}
6589
6590void QQuickTableViewResizeHandler::onGrabChanged(QQuickPointerHandler *grabber
6591 , QPointingDevice::GrabTransition transition
6592 , QPointerEvent *ev
6593 , QEventPoint &point)
6594{
6595 QQuickSinglePointHandler::onGrabChanged(grabber, transition, event: ev, point);
6596
6597 switch (transition) {
6598 case QPointingDevice::GrabPassive:
6599 case QPointingDevice::GrabExclusive:
6600 break;
6601 case QPointingDevice::UngrabPassive:
6602 case QPointingDevice::UngrabExclusive:
6603 case QPointingDevice::CancelGrabPassive:
6604 case QPointingDevice::CancelGrabExclusive:
6605 case QPointingDevice::OverrideGrabPassive:
6606 if (m_state == DraggingStarted || m_state == Dragging) {
6607 m_state = DraggingFinished;
6608 updateDrag(event: ev, point);
6609 }
6610 break;
6611 }
6612}
6613
6614bool QQuickTableViewResizeHandler::wantsEventPoint(const QPointerEvent *event, const QEventPoint &point)
6615{
6616 if (!QQuickSinglePointHandler::wantsEventPoint(event, point))
6617 return false;
6618
6619 // When the user is flicking, we disable resizing, so that
6620 // he doesn't start to resize by accident.
6621 auto tableView = static_cast<QQuickTableView *>(parentItem()->parent());
6622 return !tableView->isMoving();
6623}
6624
6625void QQuickTableViewResizeHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point)
6626{
6627 // Resolve which state we're in first...
6628 updateState(point);
6629 // ...and act on it next
6630 updateDrag(event, point);
6631}
6632
6633void QQuickTableViewResizeHandler::updateState(QEventPoint &point)
6634{
6635 auto tableView = static_cast<QQuickTableView *>(parentItem()->parent());
6636 auto tableViewPrivate = QQuickTableViewPrivate::get(q: tableView);
6637
6638 if (m_state == DraggingFinished)
6639 m_state = Listening;
6640
6641 if (point.state() == QEventPoint::Pressed) {
6642 m_row = tableViewPrivate->resizableRows ? tableViewPrivate->hoverHandler->m_row : -1;
6643 m_column = tableViewPrivate->resizableColumns ? tableViewPrivate->hoverHandler->m_column : -1;
6644 if (m_row != -1 || m_column != -1)
6645 m_state = Tracking;
6646 } else if (point.state() == QEventPoint::Released) {
6647 if (m_state == DraggingStarted || m_state == Dragging)
6648 m_state = DraggingFinished;
6649 else
6650 m_state = Listening;
6651 } else if (point.state() == QEventPoint::Updated) {
6652 switch (m_state) {
6653 case Listening:
6654 break;
6655 case Tracking: {
6656 const qreal distX = m_column != -1 ? point.position().x() - point.pressPosition().x() : 0;
6657 const qreal distY = m_row != -1 ? point.position().y() - point.pressPosition().y() : 0;
6658 const qreal dragDist = qSqrt(v: distX * distX + distY * distY);
6659 if (dragDist > qApp->styleHints()->startDragDistance())
6660 m_state = DraggingStarted;
6661 break;}
6662 case DraggingStarted:
6663 m_state = Dragging;
6664 break;
6665 case Dragging:
6666 break;
6667 case DraggingFinished:
6668 // Handled at the top of the function
6669 Q_UNREACHABLE();
6670 break;
6671 }
6672 }
6673}
6674
6675void QQuickTableViewResizeHandler::updateDrag(QPointerEvent *event, QEventPoint &point)
6676{
6677 auto tableView = static_cast<QQuickTableView *>(parentItem()->parent());
6678#if QT_CONFIG(cursor)
6679 auto tableViewPrivate = QQuickTableViewPrivate::get(q: tableView);
6680#endif
6681
6682 switch (m_state) {
6683 case Listening:
6684 break;
6685 case Tracking:
6686 setPassiveGrab(event, point, grab: true);
6687 // Disable flicking while dragging. TableView uses filtering instead of
6688 // pointer handlers to do flicking, so setting an exclusive grab (together
6689 // with grab permissions) doens't work ATM.
6690 tableView->setFiltersChildMouseEvents(false);
6691 break;
6692 case DraggingStarted:
6693 setExclusiveGrab(ev: event, point, grab: true);
6694 m_columnStartX = point.position().x();
6695 m_columnStartWidth = tableView->columnWidth(column: m_column);
6696 m_rowStartY = point.position().y();
6697 m_rowStartHeight = tableView->rowHeight(row: m_row);
6698#if QT_CONFIG(cursor)
6699 tableViewPrivate->updateCursor();
6700#endif
6701 Q_FALLTHROUGH();
6702 case Dragging: {
6703 const qreal distX = point.position().x() - m_columnStartX;
6704 const qreal distY = point.position().y() - m_rowStartY;
6705 if (m_column != -1)
6706 tableView->setColumnWidth(column: m_column, size: qMax(a: 0.001, b: m_columnStartWidth + distX));
6707 if (m_row != -1)
6708 tableView->setRowHeight(row: m_row, size: qMax(a: 0.001, b: m_rowStartHeight + distY));
6709 break; }
6710 case DraggingFinished: {
6711 tableView->setFiltersChildMouseEvents(true);
6712#if QT_CONFIG(cursor)
6713 tableViewPrivate->updateCursor();
6714#endif
6715 break; }
6716 }
6717}
6718
6719// ----------------------------------------------
6720
6721QQuickTableViewTapHandler::QQuickTableViewTapHandler(QQuickTableView *view)
6722 : QQuickTapHandler(view->contentItem())
6723{
6724 setObjectName("tableViewTapHandler");
6725}
6726
6727bool QQuickTableViewTapHandler::wantsEventPoint(const QPointerEvent *event, const QEventPoint &point)
6728{
6729 auto tableView = static_cast<QQuickTableView *>(parentItem()->parent());
6730 auto tableViewPrivate = QQuickTableViewPrivate::get(q: tableView);
6731 return tableViewPrivate->pointerNavigationEnabled && QQuickTapHandler::wantsEventPoint(event, point);
6732}
6733
6734QT_END_NAMESPACE
6735
6736#include "moc_qquicktableview_p.cpp"
6737#include "moc_qquicktableview_p_p.cpp"
6738

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