Plasma-framework

ExpandableListItem.qml
1/*
2 SPDX-FileCopyrightText: 2020 Nate Graham <nate@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7pragma ComponentBehavior: Bound
8
9import QtQuick
10import QtQuick.Layouts
11import QtQuick.Templates as T
12import org.kde.plasma.core as PlasmaCore
13import org.kde.ksvg as KSvg
14import org.kde.plasma.components as PlasmaComponents3
15import org.kde.plasma.extras as PlasmaExtras
16import org.kde.kirigami as Kirigami
17
18/**
19 * A list item that expands when clicked to show additional actions and/or a
20 * custom view.
21 * The list item has a standardized appearance, with an icon on the left badged
22 * with an optional emblem, a title and optional subtitle to the right, an
23 * optional default action button, and a button to expand and collapse the list
24 * item.
25 *
26 * When expanded, the list item shows a list of contextually-appropriate actions
27 * if contextualActions has been defined.
28 * If customExpandedViewContent has been defined, it will show a custom view.
29 * If both have been defined, it shows both, with the actions above the custom
30 * view.
31 *
32 * It is not valid to define neither; define one or both.
33 *
34 * Note: this component should only be used for lists where the maximum number
35 * of items is very low, ideally less than 10. For longer lists, consider using
36 * a different paradigm.
37 *
38 *
39 * Example usage:
40 *
41 * @code
42 * import QtQuick
43 * import QtQuick.Controls as QQC2
44 * import org.kde.kirigami as Kirigami
45 * import org.kde.plasma.extras as PlasmaExtras
46 * import org.kde.plasma.components as PlasmaComponents
47 *
48 * PlasmaComponents.ScrollView {
49 * ListView {
50 * anchors.fill: parent
51 * focus: true
52 * currentIndex: -1
53 * clip: true
54 * model: myModel
55 * highlight: PlasmaExtras.Highlight {}
56 * highlightMoveDuration: Kirigami.Units.shortDuration
57 * highlightResizeDuration: Kirigami.Units.shortDuration
58 * delegate: PlasmaExtras.ExpandableListItem {
59 * icon: model.iconName
60 * iconEmblem: model.isPaused ? "emblem-pause" : ""
61 * title: model.name
62 * subtitle: model.subtitle
63 * isDefault: model.isDefault
64 * defaultActionButtonAction: QQC2.Action {
65 * icon.name: model.isPaused ? "media-playback-start" : "media-playback-pause"
66 * text: model.isPaused ? "Resume" : "Pause"
67 * onTriggered: {
68 * if (model.isPaused) {
69 * model.resume(model.name);
70 * } else {
71 * model.pause(model.name);
72 * }
73 * }
74 * }
75 * contextualActions: [
76 * QQC2.Action {
77 * icon.name: "configure"
78 * text: "Configureā€¦"
79 * onTriggered: model.configure(model.name);
80 * }
81 * ]
82 * }
83 * }
84 * }
85 * @endcode
86 */
87Item {
88 id: listItem
89
90 /**
91 * icon: var
92 * The name of the icon used in the list item.
93 * @sa Kirigami.Icon::source
94 *
95 * Required.
96 */
97 property alias icon: listItemIcon.source
98
99 /**
100 * iconEmblem: var
101 * The name of the emblem to badge the icon with.
102 * @sa Kirigami.Icon::source
103 *
104 * Optional, defaults to nothing, in which case there is no emblem.
105 */
106 property alias iconEmblem: iconEmblem.source
107
108 /*
109 * title: string
110 * The name or title for this list item.
111 *
112 * Optional; if not defined, there will be no title and the subtitle will be
113 * vertically centered in the list item.
114 */
115 property alias title: listItemTitle.text
116
117 /*
118 * subtitle: string
119 * The subtitle for this list item, displayed under the title.
120 *
121 * Optional; if not defined, there will be no subtitle and the title will be
122 * vertically centered in the list item.
123 */
124 property alias subtitle: listItemSubtitle.text
125
126 /*
127 * subtitleCanWrap: bool
128 * Whether to allow the subtitle to become a multi-line string instead of
129 * eliding when the text is very long.
130 *
131 * Optional, defaults to false.
132 */
133 property bool subtitleCanWrap: false
134
135 /*
136 * subtitleColor: color
137 * The color of the subtitle text
138 *
139 * Optional; if not defined, the subtitle will use the default text color
140 */
141 property alias subtitleColor: listItemSubtitle.color
142
143 /*
144 * allowStyledText: bool
145 * Whether to allow the title, subtitle, and tooltip to contain styled text.
146 * For performance and security reasons, keep this off unless needed.
147 *
148 * Optional, defaults to false.
149 */
150 property bool allowStyledText: false
151
152 /*
153 * defaultActionButtonAction: T.Action
154 * The Action to execute when the default button is clicked.
155 *
156 * Optional; if not defined, no default action button will be displayed.
157 */
158 property alias defaultActionButtonAction: defaultActionButton.action
159
160 /*
161 * defaultActionButtonVisible: bool
162 * When/whether to show to default action button. Useful for making it
163 * conditionally appear or disappear.
164 *
165 * Optional; defaults to true
166 */
167 property bool defaultActionButtonVisible: true
168
169 /*
170 * showDefaultActionButtonWhenBusy : bool
171 * Whether to continue showing the default action button while the busy
172 * indicator is visible. Useful for cancelable actions that could take a few
173 * seconds and show a busy indicator while processing.
174 *
175 * Optional; defaults to false
176 */
177 property bool showDefaultActionButtonWhenBusy: false
178
179 /*
180 * contextualActions: list<T.Action>
181 * A list of standard QQC2.Action objects that describes additional actions
182 * that can be performed on this list item. For example:
183 *
184 * @code
185 * contextualActions: [
186 * Action {
187 * text: "Do something"
188 * icon.name: "document-edit"
189 * onTriggered: doSomething()
190 * },
191 * Action {
192 * text: "Do something else"
193 * icon.name: "draw-polygon"
194 * onTriggered: doSomethingElse()
195 * },
196 * Action {
197 * text: "Do something completely different"
198 * icon.name: "games-highscores"
199 * onTriggered: doSomethingCompletelyDifferent()
200 * }
201 * ]
202 * @endcode
203 *
204 * Optional; if not defined, no contextual actions will be displayed and
205 * you should instead assign a custom view to customExpandedViewContent,
206 * which will be shown when the user expands the list item.
207 */
208 property list<T.Action> contextualActions
209
210 readonly property list<T.Action> __enabledContextualActions: contextualActions.filter(action => action?.enabled ?? false)
211
212 /*
213 * A custom view to display when the user expands the list item.
214 *
215 * This component must define width and height properties. Width should be
216 * equal to the width of the list item itself, while height: will depend
217 * on the component itself.
218 *
219 * Optional; if not defined, no custom view actions will be displayed and
220 * you should instead define contextualActions, and then actions will
221 * be shown when the user expands the list item.
222 */
223 property Component customExpandedViewContent
224
225 /*
226 * The actual instance of the custom view content, if loaded.
227 * @since 5.72
228 */
229 property alias customExpandedViewContentItem: customContentLoader.item
230
231 /*
232 * isBusy: bool
233 * Whether or not to display a busy indicator on the list item. Set to true
234 * while the item should be non-interactive because things are processing.
235 *
236 * Optional; defaults to false.
237 */
238 property bool isBusy: false
239
240 /*
241 * isDefault: bool
242 * Whether or not this list item should be considered the "default" or
243 * "Current" item in the list. When set to true, and the list itself has
244 * more than one item in it, the list item's title and subtitle will be
245 * drawn in a bold style.
246 *
247 * Optional; defaults to false.
248 */
249 property bool isDefault: false
250
251 /**
252 * expanded: bool
253 * Whether the expanded view is visible.
254 *
255 * @since 5.98
256 */
257 readonly property alias expanded: expandedView.expanded
258
259 /*
260 * hasExpandableContent: bool (read-only)
261 * Whether or not this expandable list item is actually expandable. True if
262 * this item has either a custom view or else at least one enabled action.
263 * Otherwise false.
264 */
265 readonly property bool hasExpandableContent: customExpandedViewContent !== null || __enabledContextualActions.length > 0
266
267 /*
268 * expand()
269 * Show the expanded view, growing the list item to its taller size.
270 */
271 function expand() {
272 if (!listItem.hasExpandableContent) {
273 return;
274 }
275 expandedView.expanded = true
276 listItem.itemExpanded()
277 }
278
279 /*
280 * collapse()
281 * Hide the expanded view and collapse the list item to its shorter size.
282 */
283 function collapse() {
284 if (!listItem.hasExpandableContent) {
285 return;
286 }
287 expandedView.expanded = false
288 listItem.itemCollapsed()
289 }
290
291 /*
292 * toggleExpanded()
293 * Expand or collapse the list item depending on its current state.
294 */
295 function toggleExpanded() {
296 if (!listItem.hasExpandableContent) {
297 return;
298 }
299 expandedView.expanded ? listItem.collapse() : listItem.expand()
300 }
301
302 signal itemExpanded()
303 signal itemCollapsed()
304
305 width: parent ? parent.width : undefined // Assume that we will be used as a delegate, not placed in a layout
306 height: mainLayout.height
307
308 Behavior on height {
309 enabled: listItem.ListView.view.highlightResizeDuration > 0
310 SmoothedAnimation { // to match the highlight
311 id: heightAnimation
312 duration: listItem.ListView.view.highlightResizeDuration || -1
313 velocity: listItem.ListView.view.highlightResizeVelocity
314 easing.type: Easing.InOutCubic
315 }
316 }
317 clip: heightAnimation.running || expandedItemOpacityFade.running
318
319 onEnabledChanged: if (!listItem.enabled) { collapse() }
320
321 Keys.onPressed: event => {
322 if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
323 if (defaultActionButtonAction) {
324 defaultActionButtonAction.trigger()
325 } else {
326 toggleExpanded();
327 }
328 event.accepted = true;
329 } else if (event.key === Qt.Key_Escape) {
330 if (expandedView.expanded) {
331 collapse();
332 event.accepted = true;
333 }
334 // if not active, we'll let the Escape event pass through, so it can close the applet, etc.
335 } else if (event.key === Qt.Key_Space) {
336 toggleExpanded();
337 event.accepted = true;
338 }
339 }
340
341 KeyNavigation.tab: defaultActionButtonVisible ? defaultActionButton : expandToggleButton
342 KeyNavigation.right: defaultActionButtonVisible ? defaultActionButton : expandToggleButton
343 KeyNavigation.down: expandToggleButton.KeyNavigation.down
344 Keys.onDownPressed: event => {
345 if (!actionsListLoader.item || ListView.view.currentIndex < 0) {
346 ListView.view.incrementCurrentIndex();
347 ListView.view.currentItem.forceActiveFocus(Qt.TabFocusReason);
348 event.accepted = true;
349 return;
350 }
351 event.accepted = false; // Forward to KeyNavigation.down
352 }
353 Keys.onUpPressed: event => {
354 if (ListView.view.currentIndex === 0) {
355 event.accepted = false;
356 } else {
357 ListView.view.decrementCurrentIndex();
358 ListView.view.currentItem.forceActiveFocus(Qt.BacktabFocusReason);
359 }
360 }
361
362 Accessible.role: Accessible.Button
363 Accessible.name: title
364 Accessible.description: subtitle
365
366 // Handle left clicks and taps; don't accept stylus input or else it steals
367 // events from the buttons on the list item
368 TapHandler {
369 enabled: listItem.hasExpandableContent
370
371 acceptedPointerTypes: PointerDevice.Generic | PointerDevice.Finger
372
373 onSingleTapped: {
374 listItem.ListView.view.currentIndex = index
375 listItem.toggleExpanded()
376 }
377 }
378
379 MouseArea {
380 anchors.fill: parent
381
382 // This MouseArea used to intercept RightButton to open a context
383 // menu, but that has been removed, and now it's only used for hover
384 acceptedButtons: Qt.NoButton
385 hoverEnabled: true
386
387 // using onPositionChanged instead of onContainsMouseChanged so this doesn't trigger when the list reflows
388 onPositionChanged: {
389 // don't change currentIndex if it would make listview scroll
390 // see https://bugs.kde.org/show_bug.cgi?id=387797
391 // this is a workaround till https://bugreports.qt.io/browse/QTBUG-114574 gets fixed
392 // which would allow a proper solution
393 if (parent.y - listItem.ListView.view.contentY >= 0 && parent.y - listItem.ListView.view.contentY + parent.height + 1 /* border */ < listItem.ListView.view.height) {
394 listItem.ListView.view.currentIndex = (containsMouse ? index : -1)
395 }
396 }
397 onExited: if (listItem.ListView.view.currentIndex === index) {
398 listItem.ListView.view.currentIndex = -1;
399 }
400
401 ColumnLayout {
402 id: mainLayout
403
404 anchors.top: parent.top
405 anchors.left: parent.left
406 anchors.right: parent.right
407
408 spacing: 0
409
410 RowLayout {
411 id: mainRowLayout
412
413 Layout.fillWidth: true
414 Layout.margins: Kirigami.Units.smallSpacing
415 // Otherwise it becomes taller when the button appears
416 Layout.minimumHeight: defaultActionButton.height
417
418 // Icon and optional emblem
419 Kirigami.Icon {
420 id: listItemIcon
421
422 implicitWidth: Kirigami.Units.iconSizes.medium
423 implicitHeight: Kirigami.Units.iconSizes.medium
424
425 Kirigami.Icon {
426 id: iconEmblem
427
428 visible: valid
429
430 anchors.right: parent.right
431 anchors.bottom: parent.bottom
432
433 implicitWidth: Kirigami.Units.iconSizes.small
434 implicitHeight: Kirigami.Units.iconSizes.small
435 }
436 }
437
438 // Title and subtitle
439 ColumnLayout {
440 Layout.fillWidth: true
441 Layout.alignment: Qt.AlignVCenter
442
443 spacing: 0
444
445 Kirigami.Heading {
446 id: listItemTitle
447
448 visible: text.length > 0
449
450 Layout.fillWidth: true
451
452 level: 5
453
454 textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
455 elide: Text.ElideRight
456 maximumLineCount: 1
457
458 // Even if it's the default item, only make it bold when
459 // there's more than one item in the list, or else there's
460 // only one item and it's bold, which is a little bit weird
461 font.weight: listItem.isDefault && listItem.ListView.view.count > 1
462 ? Font.Bold
463 : Font.Normal
464 }
465
466 PlasmaComponents3.Label {
467 id: listItemSubtitle
468
469 visible: text.length > 0
470 font: Kirigami.Theme.smallFont
471
472 // Otherwise colored text can be hard to see
473 opacity: color === Kirigami.Theme.textColor ? 0.7 : 1.0
474
475 Layout.fillWidth: true
476
477 textFormat: listItem.allowStyledText ? Text.StyledText : Text.PlainText
478 elide: Text.ElideRight
479 maximumLineCount: subtitleCanWrap ? 9999 : 1
480 wrapMode: subtitleCanWrap ? Text.WordWrap : Text.NoWrap
481 }
482 }
483
484 // Busy indicator
485 PlasmaComponents3.BusyIndicator {
486 id: busyIndicator
487
488 visible: listItem.isBusy
489
490 // Otherwise it makes the list item taller when it appears
491 Layout.maximumHeight: defaultActionButton.implicitHeight
492 Layout.maximumWidth: Layout.maximumHeight
493 }
494
495 // Default action button
496 PlasmaComponents3.ToolButton {
497 id: defaultActionButton
498
499 visible: defaultActionButtonAction
500 && listItem.defaultActionButtonVisible
501 && (!busyIndicator.visible || listItem.showDefaultActionButtonWhenBusy)
502
503 KeyNavigation.tab: expandToggleButton
504 KeyNavigation.right: expandToggleButton
505 KeyNavigation.down: expandToggleButton.KeyNavigation.down
506 Keys.onUpPressed: event => listItem.Keys.upPressed(event)
507
508 Accessible.name: action !== null ? action.text : ""
509 }
510
511 // Expand/collapse button
512 PlasmaComponents3.ToolButton {
513 id: expandToggleButton
514 visible: listItem.hasExpandableContent
515
516 display: PlasmaComponents3.AbstractButton.IconOnly
517 text: expandedView.expanded ? i18ndc("libplasma6", "@action:button", "Collapse") : i18ndc("libplasma6", "@action:button", "Expand")
518 icon.name: expandedView.expanded ? "collapse" : "expand"
519
520 Keys.onUpPressed: event => listItem.Keys.upPressed(event)
521
522 onClicked: listItem.toggleExpanded()
523
524 PlasmaComponents3.ToolTip {
525 text: parent.text
526 }
527 }
528 }
529
530
531 // Expanded view with actions and/or custom content in it
532 Item {
533 id: expandedView
534 property bool expanded: false
535
536 Layout.preferredHeight: expanded ?
537 expandedViewLayout.implicitHeight + expandedViewLayout.anchors.topMargin + expandedViewLayout.anchors.bottomMargin : 0
538 Layout.fillWidth: true
539
540 opacity: expanded ? 1 : 0
541 Behavior on opacity {
542 enabled: listItem.ListView.view.highlightResizeDuration > 0
543 SmoothedAnimation { // to match the highlight
544 id: expandedItemOpacityFade
545 duration: listItem.ListView.view.highlightResizeDuration || -1
546 // velocity is divided by the default speed, as we're in the range 0-1
547 velocity: listItem.ListView.view.highlightResizeVelocity / 200
548 easing.type: Easing.InOutCubic
549 }
550 }
551 visible: opacity > 0
552
553 ColumnLayout {
554 id: expandedViewLayout
555 anchors.fill: parent
556 anchors.margins: Kirigami.Units.smallSpacing
557
558 spacing: Kirigami.Units.smallSpacing
559
560 // Actions list
561 Loader {
562 id: actionsListLoader
563
564 visible: status === Loader.Ready
565 active: expandedView.visible && listItem.__enabledContextualActions.length > 0
566
567 Layout.fillWidth: true
568
569 sourceComponent: Item {
570 height: childrenRect.height
571 width: actionsListLoader.width // basically, parent.width but null-proof
572
573 ColumnLayout {
574 anchors.top: parent.top
575 anchors.left: parent.left
576 anchors.right: parent.right
577 anchors.leftMargin: Kirigami.Units.gridUnit
578 anchors.rightMargin: Kirigami.Units.gridUnit
579
580 spacing: 0
581
582 Repeater {
583 id: actionRepeater
584
585 model: listItem.__enabledContextualActions
586
587 delegate: PlasmaComponents3.ToolButton {
588 required property int index
589 required property T.Action modelData
590
591 Layout.fillWidth: true
592
593 text: modelData.text
594 icon.name: modelData.icon.name
595
596 KeyNavigation.up: index > 0 ? actionRepeater.itemAt(index - 1) : expandToggleButton
597 Keys.onDownPressed: event => {
598 if (index === actionRepeater.count - 1) {
599 event.accepted = true;
600 listItem.ListView.view.incrementCurrentIndex();
601 listItem.ListView.view.currentItem.forceActiveFocus(Qt.TabFocusReason);
602 } else {
603 event.accepted = false; // Forward to KeyNavigation.down
604 }
605 }
606
607 onClicked: {
608 modelData.trigger()
609 collapse()
610 }
611 }
612 }
613 }
614 }
615 }
616
617 // Separator between the two items when both are shown
618 KSvg.SvgItem {
619 Layout.fillWidth: true
620 imagePath: "widgets/line"
621 elementId: "horizontal-line"
622 visible: actionsListLoader.visible && customContentLoader.visible
623 }
624
625 // Custom content item, if any
626 Loader {
627 id: customContentLoader
628 visible: status === Loader.Ready
629
630 Layout.fillWidth: true
631
632 active: expandedView.visible
633 asynchronous: true
634 sourceComponent: listItem.customExpandedViewContent
635 }
636 }
637 }
638 }
639 }
640}
An Item managing a Plasma-themed tooltip.
Definition tooltip.h:51
Q_SCRIPTABLE CaptureState status()
QString i18ndc(const char *domain, const char *context, const char *text, const TYPE &arg...)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
Type type(const QSqlDatabase &db)
QStringView level(QStringView ifopt)
QAction * up(const QObject *recvr, const char *slot, QObject *parent)
QString name(StandardAction id)
qsizetype length() const const
QStringList filter(QStringView str, Qt::CaseSensitivity cs) const const
qsizetype count(QChar ch, Qt::CaseSensitivity cs) const const
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 17 2024 11:54:11 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.