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