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