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

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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