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