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

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