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

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