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