1 | // Copyright (C) 2021 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 | |
4 | #include "qquicktreeviewdelegate_p.h" |
5 | |
6 | #include <QtQuickTemplates2/private/qquickitemdelegate_p_p.h> |
7 | #include <QtQuick/private/qquicktaphandler_p.h> |
8 | #include <QtQuick/private/qquicktreeview_p_p.h> |
9 | |
10 | #include <QtCore/qpointer.h> |
11 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | /*! |
15 | \qmltype TreeViewDelegate |
16 | \inherits ItemDelegate |
17 | \inqmlmodule QtQuick.Controls |
18 | \since 6.3 |
19 | \ingroup qtquickcontrols-delegates |
20 | \brief A delegate that can be assigned to a TreeView. |
21 | |
22 | \image qtquickcontrols-treeviewdelegate.png |
23 | |
24 | A TreeViewDelegate is a delegate that can be assigned to the |
25 | \l {TableView::delegate} {delegate property} of a \l TreeView. |
26 | It renders the tree, as well as the other columns, in the view |
27 | using the application style. |
28 | |
29 | \qml |
30 | TreeView { |
31 | anchors.fill: parent |
32 | delegate: TreeViewDelegate {} |
33 | // The model needs to be a QAbstractItemModel |
34 | // model: yourTreeModel |
35 | } |
36 | \endqml |
37 | |
38 | TreeViewDelegate inherits \l ItemDelegate, which means that |
39 | it's composed of three items: a \l[QML]{Control::}{background}, |
40 | a \l [QML]{Control::}{contentItem}, and an |
41 | \l [QML]{AbstractButton::}{indicator}. TreeViewDelegate takes care |
42 | of \l {indentation}{indenting} the contentItem and the indicator according |
43 | their location in the tree. The indicator will only be visible if the |
44 | delegate item is inside the \l {isTreeNode}{tree column}, and renders |
45 | a model item \l {hasChildren}{with children}. |
46 | |
47 | If you change the indicator, it will no longer be indented by default. |
48 | Instead you need to indent it yourself by setting the |
49 | \l [QML] {Item::x}{x position} of the indicator, taking the \l depth and |
50 | \l indentation into account. Below is an example of how to do that: |
51 | |
52 | \qml |
53 | TreeViewDelegate { |
54 | indicator: Item { |
55 | x: leftMargin + (depth * indentation) |
56 | } |
57 | } |
58 | \endqml |
59 | |
60 | The position of the contentItem is controlled with \l [QML]{Control::}{padding}. |
61 | This means that you can change the contentItem without dealing with indentation. |
62 | But it also means that you should avoid changing padding to something else, as |
63 | that will remove the indentation. The space to the left of the indicator is instead |
64 | controlled with \l leftMargin. The space between the indicator and the contentItem |
65 | is controlled with \l [QML]{Control::}{spacing}. And the space to the right of the |
66 | contentItem is controlled with \l rightMargin. |
67 | |
68 | \section2 Interacting with pointers |
69 | TreeViewDelegate inherits \l ItemDelegate. This means that it will emit signals |
70 | such as \l {AbstractButton::clicked()}{clicked} when the user clicks on the delegate. |
71 | If needed, you could connect to that signal to implement application specific |
72 | functionality, in addition to the default expand/collapse behavior (and even set \l |
73 | {TableView::pointerNavigationEnabled}{pointerNavigationEnabled} to \c false, to |
74 | disable the default behavior as well). |
75 | |
76 | But the ItemDelegate API does not give you information about the position of the |
77 | click, or which modifiers are being held. If this is needed, a better approach would |
78 | be to use pointer handlers, for example: |
79 | |
80 | \qml |
81 | TreeView { |
82 | id: treeView |
83 | delegate: TreeViewDelegate { |
84 | TapHandler { |
85 | acceptedButtons: Qt.RightButton |
86 | onTapped: someContextMenu.open() |
87 | } |
88 | |
89 | TapHandler { |
90 | acceptedModifiers: Qt.ControlModifier |
91 | onTapped: { |
92 | if (treeView.isExpanded(row)) |
93 | treeView.collapseRecursively(row) |
94 | else |
95 | treeView.expandRecursively(row) |
96 | } |
97 | } |
98 | } |
99 | } |
100 | \endqml |
101 | |
102 | \note If you want to disable the default behavior that occurs when the |
103 | user clicks on the delegate (like changing the current index), you can set |
104 | \l {TableView::pointerNavigationEnabled}{pointerNavigationEnabled} to \c false. |
105 | |
106 | \section2 Editing nodes in the tree |
107 | TreeViewDelegate has a default \l {TableView::editDelegate}{edit delegate} |
108 | assigned. If \l TreeView has \l {TableView::editTriggers}{edit triggers} |
109 | set, and the \l {TableView::model}{model} has support for |
110 | \l {Editing cells} {editing model items}, then the user can activate any of |
111 | the edit triggers to edit the text of the \l {TreeViewDelegate::current}{current} |
112 | tree node. |
113 | |
114 | The default edit delegate will try to use the \c {Qt.EditRole} to read and |
115 | write data to the \l {TableView::model}{model}. |
116 | If \l QAbstractItemModel::data() returns an empty string for this role, then |
117 | \c {Qt.DisplayRole} will be used instead. |
118 | |
119 | You can always assign your own custom edit delegate to |
120 | \l {TableView::editDelegate}{TableView.editDelegate} if you have needs |
121 | outside what the default edit delegate offers. |
122 | |
123 | \sa TreeView |
124 | */ |
125 | |
126 | /*! |
127 | \qmlproperty TreeView QtQuick.Controls::TreeViewDelegate::treeView |
128 | |
129 | This property points to the \l TreeView that contains the delegate item. |
130 | */ |
131 | |
132 | /*! |
133 | \qmlproperty bool QtQuick.Controls::TreeViewDelegate::hasChildren |
134 | |
135 | This property is \c true if the model item drawn by the delegate |
136 | has children in the model. |
137 | */ |
138 | |
139 | /*! |
140 | \qmlproperty bool QtQuick.Controls::TreeViewDelegate::isTreeNode |
141 | |
142 | This property is \c true if the delegate item draws a node in the tree. |
143 | Only one column in the view will be used to draw the tree, and |
144 | therefore, only delegate items in that column will have this |
145 | property set to \c true. |
146 | |
147 | A node in the tree is \l{indentation}{indented} according to its \c depth, and show |
148 | an \l [QML]{AbstractButton::}{indicator} if hasChildren is \c true. Delegate items |
149 | in other columns will have this property set to \c false. |
150 | */ |
151 | |
152 | /*! |
153 | \qmlproperty bool QtQuick.Controls::TreeViewDelegate::expanded |
154 | |
155 | This property is \c true if the model item drawn by the delegate |
156 | is expanded in the view. |
157 | */ |
158 | |
159 | /*! |
160 | \qmlproperty int QtQuick.Controls::TreeViewDelegate::depth |
161 | |
162 | This property holds the depth of the model item drawn by the delegate. |
163 | The depth of a model item is the same as the number of ancestors |
164 | it has in the model. |
165 | */ |
166 | |
167 | /*! |
168 | \qmlproperty bool QtQuick.Controls::TreeViewDelegate::current |
169 | |
170 | This property holds if the delegate represent the |
171 | \l {QItemSelectionModel::currentIndex()}{current index} |
172 | in the \l {TableView::selectionModel}{selection model}. |
173 | */ |
174 | |
175 | /*! |
176 | \qmlproperty bool QtQuick.Controls::TreeViewDelegate::selected |
177 | |
178 | This property holds if the delegate represent a |
179 | \l {QItemSelectionModel::selection()}{selected index} |
180 | in the \l {TableView::selectionModel}{selection model}. |
181 | */ |
182 | |
183 | /*! |
184 | \qmlproperty bool QtQuick.Controls::TreeViewDelegate::editing |
185 | \since 6.5 |
186 | |
187 | This property holds if the delegate is being |
188 | \l {Editing cells}{edited.} |
189 | */ |
190 | |
191 | /*! |
192 | \qmlproperty real QtQuick.Controls::TreeViewDelegate::indentation |
193 | |
194 | This property holds the space a child is indented horizontally |
195 | relative to its parent. |
196 | */ |
197 | |
198 | /*! |
199 | \qmlproperty real QtQuick.Controls::TreeViewDelegate::leftMargin |
200 | |
201 | This property holds the space between the left edge of the view |
202 | and the left edge of the indicator (in addition to indentation). |
203 | If no indicator is visible, the space will be between the left |
204 | edge of the view and the left edge of the contentItem. |
205 | |
206 | \sa rightMargin, indentation, {QQuickControl::}{spacing} |
207 | */ |
208 | |
209 | /*! |
210 | \qmlproperty real QtQuick.Controls::TreeViewDelegate::rightMargin |
211 | |
212 | This property holds the space between the right edge of the view |
213 | and the right edge of the contentItem. |
214 | |
215 | \sa leftMargin, indentation, {QQuickControl::}{spacing} |
216 | */ |
217 | |
218 | using namespace Qt::Literals::StringLiterals; |
219 | |
220 | class QQuickTreeViewDelegatePrivate : public QQuickItemDelegatePrivate |
221 | { |
222 | public: |
223 | Q_DECLARE_PUBLIC(QQuickTreeViewDelegate) |
224 | |
225 | void updateIndicatorVisibility(); |
226 | void updateIndicatorPointerHandlers(); |
227 | void toggleExpanded(); |
228 | QPalette defaultPalette() const override; |
229 | |
230 | public: |
231 | QPointer<QQuickTreeView> m_treeView; |
232 | QPointer<QQuickTapHandler> m_tapHandlerOnIndicator; |
233 | qreal m_indentation = 18; |
234 | qreal m_leftMargin = 0; |
235 | qreal m_rightMargin = 0; |
236 | bool m_isTreeNode = false; |
237 | bool m_expanded = false; |
238 | bool m_current = false; |
239 | bool m_selected = false; |
240 | bool m_editing = false; |
241 | bool m_hasChildren = false; |
242 | bool m_pressOnTopOfIndicator = false; |
243 | int m_depth = 0; |
244 | }; |
245 | |
246 | void QQuickTreeViewDelegatePrivate::toggleExpanded() |
247 | { |
248 | Q_Q(QQuickTreeViewDelegate); |
249 | |
250 | auto view = q->treeView(); |
251 | if (!view) |
252 | return; |
253 | if (!view->pointerNavigationEnabled()) |
254 | return; |
255 | |
256 | const int row = qmlContext(q)->contextProperty(u"row"_s).toInt(); |
257 | view->toggleExpanded(row); |
258 | } |
259 | |
260 | void QQuickTreeViewDelegatePrivate::updateIndicatorPointerHandlers() |
261 | { |
262 | Q_Q(QQuickTreeViewDelegate); |
263 | |
264 | // Remove the tap handler that was installed |
265 | // on the previous indicator |
266 | delete m_tapHandlerOnIndicator.data(); |
267 | |
268 | auto indicator = q->indicator(); |
269 | if (!indicator) |
270 | return; |
271 | |
272 | m_tapHandlerOnIndicator = new QQuickTapHandler(indicator); |
273 | m_tapHandlerOnIndicator->setAcceptedModifiers(Qt::NoModifier); |
274 | // Work-around to block taps from passing through to TreeView. |
275 | m_tapHandlerOnIndicator->setGesturePolicy(QQuickTapHandler::ReleaseWithinBounds); |
276 | connect(sender: m_tapHandlerOnIndicator, signal: &QQuickTapHandler::tapped, receiverPrivate: this, slot: &QQuickTreeViewDelegatePrivate::toggleExpanded); |
277 | } |
278 | |
279 | void QQuickTreeViewDelegatePrivate::updateIndicatorVisibility() |
280 | { |
281 | Q_Q(QQuickTreeViewDelegate); |
282 | |
283 | if (auto indicator = q_func()->indicator()) { |
284 | const bool insideDelegateBounds = indicator->x() + indicator->width() < q->width(); |
285 | indicator->setVisible(m_isTreeNode && m_hasChildren && insideDelegateBounds); |
286 | } |
287 | } |
288 | |
289 | QQuickTreeViewDelegate::QQuickTreeViewDelegate(QQuickItem *parent) |
290 | : QQuickItemDelegate(*(new QQuickTreeViewDelegatePrivate), parent) |
291 | { |
292 | Q_D(QQuickTreeViewDelegate); |
293 | |
294 | auto tapHandler = new QQuickTapHandler(this); |
295 | tapHandler->setAcceptedModifiers(Qt::NoModifier); |
296 | QObjectPrivate::connect(sender: this, signal: &QQuickAbstractButton::indicatorChanged, receiverPrivate: d, slot: &QQuickTreeViewDelegatePrivate::updateIndicatorPointerHandlers); |
297 | |
298 | // Since we override mousePressEvent to avoid QQuickAbstractButton from blocking |
299 | // pointer handlers, we inform the button about its pressed state from the tap |
300 | // handler instead. This will ensure that we emit button signals like |
301 | // pressed, clicked, and doubleClicked. |
302 | connect(sender: tapHandler, signal: &QQuickTapHandler::pressedChanged, slot: [this, d, tapHandler] { |
303 | auto view = treeView(); |
304 | if (view && !view->pointerNavigationEnabled()) |
305 | return; |
306 | |
307 | const QQuickHandlerPoint p = tapHandler->point(); |
308 | if (tapHandler->isPressed()) |
309 | d->handlePress(point: p.position(), timestamp: 0); |
310 | else if (tapHandler->tapCount() > 0) |
311 | d->handleRelease(point: p.position(), timestamp: 0); |
312 | else |
313 | d->handleUngrab(); |
314 | |
315 | if (tapHandler->tapCount() > 1 && !tapHandler->isPressed()) |
316 | emit doubleClicked(); |
317 | }); |
318 | } |
319 | |
320 | void QQuickTreeViewDelegate::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) |
321 | { |
322 | Q_D(QQuickTreeViewDelegate); |
323 | |
324 | QQuickItemDelegate::geometryChange(newGeometry, oldGeometry); |
325 | d->updateIndicatorVisibility(); |
326 | } |
327 | |
328 | void QQuickTreeViewDelegate::mousePressEvent(QMouseEvent *event) |
329 | { |
330 | Q_D(QQuickTreeViewDelegate); |
331 | |
332 | const auto view = d->m_treeView; |
333 | if (view && view->pointerNavigationEnabled()) { |
334 | // Ignore mouse events so that we don't block our own pointer handlers, or |
335 | // pointer handlers in e.g TreeView, TableView, or SelectionRectangle. Instead |
336 | // we call out to the needed mouse handling functions in QAbstractButton directly |
337 | // from our pointer handlers, to ensure that continue to work as a button. |
338 | event->ignore(); |
339 | return; |
340 | } |
341 | |
342 | QQuickItemDelegate::mousePressEvent(event); |
343 | } |
344 | |
345 | QPalette QQuickTreeViewDelegatePrivate::defaultPalette() const |
346 | { |
347 | return QQuickTheme::palette(scope: QQuickTheme::ItemView); |
348 | } |
349 | |
350 | QFont QQuickTreeViewDelegate::defaultFont() const |
351 | { |
352 | return QQuickTheme::font(scope: QQuickTheme::ItemView); |
353 | } |
354 | |
355 | qreal QQuickTreeViewDelegate::indentation() const |
356 | { |
357 | return d_func()->m_indentation; |
358 | } |
359 | |
360 | void QQuickTreeViewDelegate::setIndentation(qreal indentation) |
361 | { |
362 | Q_D(QQuickTreeViewDelegate); |
363 | if (qFuzzyCompare(p1: d->m_indentation, p2: indentation)) |
364 | return; |
365 | |
366 | d->m_indentation = indentation; |
367 | emit indentationChanged(); |
368 | } |
369 | |
370 | bool QQuickTreeViewDelegate::isTreeNode() const |
371 | { |
372 | return d_func()->m_isTreeNode; |
373 | } |
374 | |
375 | void QQuickTreeViewDelegate::setIsTreeNode(bool isTreeNode) |
376 | { |
377 | Q_D(QQuickTreeViewDelegate); |
378 | if (d->m_isTreeNode == isTreeNode) |
379 | return; |
380 | |
381 | d->m_isTreeNode = isTreeNode; |
382 | d->updateIndicatorVisibility(); |
383 | emit isTreeNodeChanged(); |
384 | } |
385 | |
386 | bool QQuickTreeViewDelegate::hasChildren() const |
387 | { |
388 | return d_func()->m_hasChildren; |
389 | } |
390 | |
391 | void QQuickTreeViewDelegate::setHasChildren(bool hasChildren) |
392 | { |
393 | Q_D(QQuickTreeViewDelegate); |
394 | if (d->m_hasChildren == hasChildren) |
395 | return; |
396 | |
397 | d->m_hasChildren = hasChildren; |
398 | d->updateIndicatorVisibility(); |
399 | emit hasChildrenChanged(); |
400 | } |
401 | |
402 | bool QQuickTreeViewDelegate::expanded() const |
403 | { |
404 | return d_func()->m_expanded; |
405 | } |
406 | |
407 | void QQuickTreeViewDelegate::setExpanded(bool expanded) |
408 | { |
409 | Q_D(QQuickTreeViewDelegate); |
410 | if (d->m_expanded == expanded) |
411 | return; |
412 | |
413 | d->m_expanded = expanded; |
414 | emit expandedChanged(); |
415 | } |
416 | |
417 | bool QQuickTreeViewDelegate::current() const |
418 | { |
419 | return d_func()->m_current; |
420 | } |
421 | |
422 | void QQuickTreeViewDelegate::setCurrent(bool current) |
423 | { |
424 | Q_D(QQuickTreeViewDelegate); |
425 | if (d->m_current == current) |
426 | return; |
427 | |
428 | d->m_current = current; |
429 | emit currentChanged(); |
430 | } |
431 | |
432 | bool QQuickTreeViewDelegate::selected() const |
433 | { |
434 | return d_func()->m_selected; |
435 | } |
436 | |
437 | void QQuickTreeViewDelegate::setSelected(bool selected) |
438 | { |
439 | Q_D(QQuickTreeViewDelegate); |
440 | if (d->m_selected == selected) |
441 | return; |
442 | |
443 | d->m_selected = selected; |
444 | emit selectedChanged(); |
445 | } |
446 | |
447 | bool QQuickTreeViewDelegate::editing() const |
448 | { |
449 | return d_func()->m_editing; |
450 | } |
451 | |
452 | void QQuickTreeViewDelegate::setEditing(bool editing) |
453 | { |
454 | Q_D(QQuickTreeViewDelegate); |
455 | if (d->m_editing == editing) |
456 | return; |
457 | |
458 | d->m_editing = editing; |
459 | emit editingChanged(); |
460 | } |
461 | |
462 | int QQuickTreeViewDelegate::depth() const |
463 | { |
464 | return d_func()->m_depth; |
465 | } |
466 | |
467 | void QQuickTreeViewDelegate::setDepth(int depth) |
468 | { |
469 | Q_D(QQuickTreeViewDelegate); |
470 | if (d->m_depth == depth) |
471 | return; |
472 | |
473 | d->m_depth = depth; |
474 | emit depthChanged(); |
475 | } |
476 | |
477 | QQuickTreeView *QQuickTreeViewDelegate::treeView() const |
478 | { |
479 | return d_func()->m_treeView; |
480 | } |
481 | |
482 | void QQuickTreeViewDelegate::setTreeView(QQuickTreeView *treeView) |
483 | { |
484 | Q_D(QQuickTreeViewDelegate); |
485 | if (d->m_treeView == treeView) |
486 | return; |
487 | |
488 | d->m_treeView = treeView; |
489 | emit treeviewChanged(); |
490 | } |
491 | |
492 | void QQuickTreeViewDelegate::componentComplete() |
493 | { |
494 | Q_D(QQuickTreeViewDelegate); |
495 | QQuickItemDelegate::componentComplete(); |
496 | d->updateIndicatorVisibility(); |
497 | d->updateIndicatorPointerHandlers(); |
498 | } |
499 | |
500 | qreal QQuickTreeViewDelegate::leftMargin() const |
501 | { |
502 | return d_func()->m_leftMargin; |
503 | } |
504 | |
505 | void QQuickTreeViewDelegate::setLeftMargin(qreal leftMargin) |
506 | { |
507 | Q_D(QQuickTreeViewDelegate); |
508 | if (qFuzzyCompare(p1: d->m_leftMargin, p2: leftMargin)) |
509 | return; |
510 | |
511 | d->m_leftMargin = leftMargin; |
512 | emit leftMarginChanged(); |
513 | } |
514 | |
515 | qreal QQuickTreeViewDelegate::rightMargin() const |
516 | { |
517 | return d_func()->m_rightMargin; |
518 | } |
519 | |
520 | void QQuickTreeViewDelegate::setRightMargin(qreal rightMargin) |
521 | { |
522 | Q_D(QQuickTreeViewDelegate); |
523 | if (qFuzzyCompare(p1: d->m_rightMargin, p2: rightMargin)) |
524 | return; |
525 | |
526 | d->m_rightMargin = rightMargin; |
527 | emit rightMarginChanged(); |
528 | } |
529 | |
530 | QT_END_NAMESPACE |
531 | |
532 | #include "moc_qquicktreeviewdelegate_p.cpp" |
533 |
Definitions
- QQuickTreeViewDelegatePrivate
- toggleExpanded
- updateIndicatorPointerHandlers
- updateIndicatorVisibility
- QQuickTreeViewDelegate
- geometryChange
- mousePressEvent
- defaultPalette
- defaultFont
- indentation
- setIndentation
- isTreeNode
- setIsTreeNode
- hasChildren
- setHasChildren
- expanded
- setExpanded
- current
- setCurrent
- selected
- setSelected
- editing
- setEditing
- depth
- setDepth
- treeView
- setTreeView
- componentComplete
- leftMargin
- setLeftMargin
- rightMargin
Learn Advanced QML with KDAB
Find out more