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