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