Kirigami2

SwipeListItem.qml
1/*
2 * SPDX-FileCopyrightText: 2019 Marco Martin <notmart@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7import QtQuick
8import QtQuick.Controls as QQC2
9import QtQuick.Layouts
10import QtQuick.Templates as T
11import org.kde.kirigami as Kirigami
12import "private"
13
14/**
15 * An item delegate intended to support extra actions obtainable
16 * by uncovering them by dragging away the item with the handle.
17 *
18 * This acts as a container for normal list items.
19 *
20 * Example usage:
21 * @code
22 * ListView {
23 * model: myModel
24 * delegate: SwipeListItem {
25 * QQC2.Label {
26 * text: model.text
27 * }
28 * actions: [
29 * Action {
30 * icon.name: "document-decrypt"
31 * onTriggered: print("Action 1 clicked")
32 * },
33 * Action {
34 * icon.name: model.action2Icon
35 * onTriggered: //do something
36 * }
37 * ]
38 * }
39 *
40 * }
41 * @endcode
42 *
43 * @inherit QtQuick.Templates.SwipeDelegate
44 */
45QQC2.SwipeDelegate {
46 id: listItem
47
48//BEGIN properties
49 /**
50 * @brief This property sets whether the item should emit signals related to mouse interaction.
51 *
52 * default: ``true``
53 *
54 * @deprecated Use hoverEnabled instead.
55 * @property bool supportsMouseEvents
56 */
57 property alias supportsMouseEvents: listItem.hoverEnabled
58
59 /**
60 * @brief This property tells whether the cursor is currently hovering over the item.
61 *
62 * On mobile touch devices, this will be true only when pressed.
63 *
64 * @see QtQuick.Templates.ItemDelegate::hovered
65 * @deprecated This will be removed in KF6; use the ``hovered`` property instead.
66 * @property bool containsMouse
67 */
68 readonly property alias containsMouse: listItem.hovered
69
70 /**
71 * @brief This property sets whether instances of this list item will alternate
72 * between two colors, helping readability.
73 *
74 * It is suggested to use this only when implementing a view with multiple columns.
75 *
76 * default: ``false``
77 *
78 * @since 2.7
79 */
80 property bool alternatingBackground: false
81
82 /**
83 * @brief This property sets whether this item is a section delegate.
84 *
85 * Setting this to true will make the list item look like a "title" for items under it.
86 *
87 * default: ``false``
88 *
89 * @see ListSectionHeader
90 */
91 property bool sectionDelegate: false
92
93 /**
94 * @brief This property sets whether the separator is visible.
95 *
96 * The separator is a line between this and the item under it.
97 *
98 * default: ``false``
99 */
100 property bool separatorVisible: false
101
102 /**
103 * @brief This property holds the background color of the list item.
104 *
105 * It is advised to use the default value.
106 * default: ``Kirigami.Theme.backgroundColor``
107 */
108 property color backgroundColor: Kirigami.Theme.backgroundColor
109
110 /**
111 * @brief This property holds the background color to be used when
112 * background alternating is enabled.
113 *
114 * It is advised to use the default value.
115 * default: ``Kirigami.Theme.alternateBackgroundColor``
116 *
117 * @since 2.7
118 */
119 property color alternateBackgroundColor: Kirigami.Theme.alternateBackgroundColor
120
121 /**
122 * @brief This property holds the color of the background
123 * when the item is pressed or selected.
124 *
125 * It is advised to use the default value.
126 * default: ``Kirigami.Theme.highlightColor``
127 */
128 property color activeBackgroundColor: Kirigami.Theme.highlightColor
129
130 /**
131 * @brief This property holds the color of the text in the item.
132 *
133 * It is advised to use the default value.
134 * default: ``Theme.textColor``
135 *
136 * If custom text elements are inserted in a SwipeListItem,
137 * their color will have to be manually set with this property.
138 */
139 property color textColor: Kirigami.Theme.textColor
140
141 /**
142 * @brief This property holds the color of the text when the item is pressed or selected.
143 *
144 * It is advised to use the default value.
145 * default: ``Kirigami.Theme.highlightedTextColor``
146 *
147 * If custom text elements are inserted in a SwipeListItem,
148 * their color property will have to be manually bound with this property
149 */
150 property color activeTextColor: Kirigami.Theme.highlightedTextColor
151
152 /**
153 * @brief This property tells whether actions are visible and interactive.
154 *
155 * True if it's possible to see and interact with the item's actions.
156 *
157 * Actions become hidden while editing of an item, for example.
158 *
159 * @since 2.5
160 */
161 readonly property bool actionsVisible: actionsLayout.hasVisibleActions
162
163 /**
164 * @brief This property sets whether actions behind this SwipeListItem will always be visible.
165 *
166 * default: `true in desktop and tablet mode`
167 *
168 * @since 2.15
169 */
170 property bool alwaysVisibleActions: !Kirigami.Settings.isMobile
171
172 /**
173 * @brief This property holds actions of the list item.
175 * At most 4 actions can be revealed when sliding away the list item;
176 * others will be shown in the overflow menu.
177 */
178 property list<T.Action> actions
179
180 /**
181 * @brief This property holds the width of the overlay.
182 *
183 * The value can represent the width of the handle component or the action layout.
184 *
185 * @since 2.19
186 * @property real overlayWidth
187 */
188 readonly property alias overlayWidth: overlayLoader.width
189
190//END properties
191
192 property ListView listView;
193
194 LayoutMirroring.childrenInherit: true
195
196 hoverEnabled: true
197 implicitWidth: contentItem ? implicitContentWidth : Kirigami.Units.gridUnit * 12
198 width: parent ? parent.width : implicitWidth
199 implicitHeight: Math.max(Kirigami.Units.gridUnit * 2, implicitContentHeight) + topPadding + bottomPadding
200
201 padding: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
202
203 leftPadding: padding * 2 + (mirrored ? overlayLoader.paddingOffset : 0)
204 rightPadding: padding * 2 + (mirrored ? 0 : overlayLoader.paddingOffset)
205
206 topPadding: padding
207 bottomPadding: padding
208
209 Keys.onTabPressed: (event) => {
210 if (actionsLayout.hasVisibleActions) {
211 actionsLayout.children[0].tabbedFromDelegate = true
212 actionsLayout.children[0].forceActiveFocus(Qt.TabFocusReason)
213 } else {
214 event.accepted = false
215 }
216 }
217
218 Keys.onPressed: (event) => {
219 if ((actionsLayout.hasVisibleActions && activeFocus && event.key == Qt.Key_Right && Qt.application.layoutDirection == Qt.LeftToRight) ||
220 (actionsLayout.hasVisibleActions && activeFocus && event.key == Qt.Key_Left && Qt.application.layoutDirection == Qt.RightToLeft)) {
221 for (var target = 0; target < actionsRep.count; target ++) {
222 if (actionsLayout.children[target].visible) {
223 break
224 }
225 }
226 if (target < actionsRep.count) {
227 actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason)
228 event.accepted = true
229 }
230 }
231 }
232
233 QtObject {
234 id: internal
235
236 property Flickable view: listItem.ListView.view || (listItem.parent ? (listItem.parent.ListView.view || (listItem.parent instanceof Flickable ? listItem.parent : null)) : null)
237
238 function viewHasPropertySwipeFilter(): bool {
239 return view && view.parent && view.parent.parent && "_swipeFilter" in view.parent.parent;
240 }
241
242 readonly property QtObject swipeFilterItem: (viewHasPropertySwipeFilter() && view.parent.parent._swipeFilter) ? view.parent.parent._swipeFilter : null
243
244 readonly property bool edgeEnabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem || swipeFilterItem.currentItem === listItem.parent : false
245
246 // install the SwipeItemEventFilter
247 onViewChanged: {
248 if (listItem.alwaysVisibleActions || !Kirigami.Settings.tabletMode) {
249 return;
250 }
251 if (viewHasPropertySwipeFilter() && Kirigami.Settings.tabletMode && !internal.view.parent.parent._swipeFilter) {
252 const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml"));
253 internal.view.parent.parent._swipeFilter = component.createObject(internal.view.parent.parent);
254 component.destroy();
255 }
256 }
257 }
258
259 Connections {
260 target: Kirigami.Settings
261 function onTabletModeChanged() {
262 if (!internal.viewHasPropertySwipeFilter()) {
263 return;
264 }
265 if (Kirigami.Settings.tabletMode) {
266 if (!internal.swipeFilterItem) {
267 const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml"));
268 listItem.ListView.view.parent.parent._swipeFilter = component.createObject(listItem.ListView.view.parent.parent);
269 component.destroy();
270 }
271 } else {
272 if (listItem.ListView.view.parent.parent._swipeFilter) {
273 listItem.ListView.view.parent.parent._swipeFilter.destroy();
274 slideAnim.to = 0;
275 slideAnim.restart();
276 }
277 }
278 }
279 }
280
281//BEGIN items
282 Loader {
283 id: overlayLoader
284 readonly property int paddingOffset: (visible ? width : 0) + Kirigami.Units.smallSpacing
285 readonly property var theAlias: anchors
286 function validate(want, defaultValue) {
287 const expectedLeftPadding = () => listItem.padding * 2 + (listItem.mirrored ? overlayLoader.paddingOffset : 0)
288 const expectedRightPadding = () => listItem.padding * 2 + (listItem.mirrored ? 0 : overlayLoader.paddingOffset)
289
290 const warningText =
291 `Don't override the leftPadding or rightPadding on a SwipeListItem!\n` +
292 `This makes it impossible for me to adjust my layout as I need to for various usecases.\n` +
293 `I'll try to fix the mistake for you, but you should remove your overrides from your app's code entirely.\n` +
294 `If I can't fix the paddings, I'll fall back to a default layout, but it'll be slightly incorrect and lacks\n` +
295 `adaptations needed for touch screens and right-to-left languages, among other things.`
296
297 if (listItem.leftPadding != expectedLeftPadding() || listItem.rightPadding != expectedRightPadding()) {
298 listItem.leftPadding = Qt.binding(expectedLeftPadding)
299 listItem.rightPadding = Qt.binding(expectedRightPadding)
300 console.warn(warningText)
301 return defaultValue
302 }
303
304 return want
305 }
306 anchors {
307 right: validate(listItem.mirrored ? undefined : (contentItem ? contentItem.right : undefined), contentItem ? contentItem.right : undefined)
308 rightMargin: validate(-paddingOffset, 0)
309 left: validate(!listItem.mirrored ? undefined : (contentItem ? contentItem.left : undefined), undefined)
310 leftMargin: validate(-paddingOffset, 0)
311 top: parent.top
312 bottom: parent.bottom
313 }
314 LayoutMirroring.enabled: false
315
316 parent: listItem
317 z: contentItem ? contentItem.z + 1 : 0
318 width: item ? item.implicitWidth : actionsLayout.implicitWidth
319 active: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode
320 visible: listItem.actionsVisible && opacity > 0
321 asynchronous: true
322 sourceComponent: handleComponent
323 opacity: listItem.alwaysVisibleActions || Kirigami.Settings.tabletMode || listItem.hovered ? 1 : 0
324 Behavior on opacity {
325 OpacityAnimator {
326 id: opacityAnim
327 duration: Kirigami.Units.veryShortDuration
328 easing.type: Easing.InOutQuad
329 }
330 }
331 }
332
333 Component {
334 id: handleComponent
335
336 MouseArea {
337 id: dragButton
338 anchors {
339 right: parent.right
340 }
341 implicitWidth: Kirigami.Units.iconSizes.smallMedium
342
343 preventStealing: true
344 readonly property real openPosition: (listItem.width - width - listItem.leftPadding * 2)/listItem.width
345 property real startX: 0
346 property real lastPosition: 0
347 property bool openIntention
348
349 onPressed: mouse => {
350 startX = mapToItem(listItem, 0, 0).x;
351 }
352 onClicked: mouse => {
353 if (Math.abs(mapToItem(listItem, 0, 0).x - startX) > Qt.styleHints.startDragDistance) {
354 return;
355 }
356 if (listItem.mirrored) {
357 if (listItem.swipe.position < 0.5) {
358 slideAnim.to = openPosition
359 } else {
360 slideAnim.to = 0
361 }
362 } else {
363 if (listItem.swipe.position > -0.5) {
364 slideAnim.to = -openPosition
365 } else {
366 slideAnim.to = 0
367 }
368 }
369 slideAnim.restart();
370 }
371 onPositionChanged: mouse => {
372 const pos = mapToItem(listItem, mouse.x, mouse.y);
373
374 if (listItem.mirrored) {
375 listItem.swipe.position = Math.max(0, Math.min(openPosition, (pos.x / listItem.width)));
376 openIntention = listItem.swipe.position > lastPosition;
377 } else {
378 listItem.swipe.position = Math.min(0, Math.max(-openPosition, (pos.x / (listItem.width -listItem.rightPadding) - 1)));
379 openIntention = listItem.swipe.position < lastPosition;
380 }
381 lastPosition = listItem.swipe.position;
382 }
383 onReleased: mouse => {
384 if (listItem.mirrored) {
385 if (openIntention) {
386 slideAnim.to = openPosition
387 } else {
388 slideAnim.to = 0
389 }
390 } else {
391 if (openIntention) {
392 slideAnim.to = -openPosition
393 } else {
394 slideAnim.to = 0
395 }
396 }
397 slideAnim.restart();
398 }
399
400 Kirigami.Icon {
401 id: handleIcon
402 anchors.fill: parent
403 selected: listItem.checked || (listItem.down && !listItem.checked && !listItem.sectionDelegate)
404 source: (listItem.mirrored ? (listItem.background.x < listItem.background.width/2 ? "overflow-menu-right" : "overflow-menu-left") : (listItem.background.x < -listItem.background.width/2 ? "overflow-menu-right" : "overflow-menu-left"))
405 }
406
407 Connections {
408 id: swipeFilterConnection
409
410 target: internal.edgeEnabled ? internal.swipeFilterItem : null
411 function onPeekChanged() {
412 if (!listItem.actionsVisible) {
413 return;
414 }
415
416 if (listItem.mirrored) {
417 listItem.swipe.position = Math.max(0, Math.min(dragButton.openPosition, internal.swipeFilterItem.peek));
418 dragButton.openIntention = listItem.swipe.position > dragButton.lastPosition;
419
420 } else {
421 listItem.swipe.position = Math.min(0, Math.max(-dragButton.openPosition, -internal.swipeFilterItem.peek));
422 dragButton.openIntention = listItem.swipe.position < dragButton.lastPosition;
423 }
424
425 dragButton.lastPosition = listItem.swipe.position;
426 }
427 function onPressed(mouse) {
428 if (internal.edgeEnabled) {
429 dragButton.pressed(mouse);
430 }
431 }
432 function onClicked(mouse) {
433 if (Math.abs(listItem.background.x) < Kirigami.Units.gridUnit && internal.edgeEnabled) {
434 dragButton.clicked(mouse);
435 }
436 }
437 function onReleased(mouse) {
438 if (internal.edgeEnabled) {
439 dragButton.released(mouse);
440 }
441 }
442 function onCurrentItemChanged() {
443 if (!internal.edgeEnabled) {
444 slideAnim.to = 0;
445 slideAnim.restart();
446 }
447 }
448 }
449 }
450 }
451
452 // TODO: expose in API?
453 Component {
454 id: actionsBackgroundDelegate
455 Item {
456 anchors.fill: parent
457 z: 1
458
459 readonly property Item contentItem: swipeBackground
460 Rectangle {
461 id: swipeBackground
462 anchors {
463 top: parent.top
464 bottom: parent.bottom
465 }
466 clip: true
467 color: parent.pressed ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Qt.darker(Kirigami.Theme.backgroundColor, 1.05)
468 x: listItem.mirrored ? listItem.background.x - width : (listItem.background.x + listItem.background.width)
469 width: listItem.mirrored ? parent.width - (parent.width - x) : parent.width - x
470
471 TapHandler {
472 onTapped: listItem.swipe.close()
473 }
474 EdgeShadow {
475 edge: Qt.TopEdge
476 visible: background.x != 0
477 anchors {
478 right: parent.right
479 left: parent.left
480 top: parent.top
481 }
482 }
483 EdgeShadow {
484 edge: listItem.mirrored ? Qt.RightEdge : Qt.LeftEdge
485
486 visible: background.x != 0
487 anchors {
488 top: parent.top
489 bottom: parent.bottom
490 }
491 }
492 }
493
494 visible: listItem.swipe.position != 0
495 }
496 }
497
498
499 RowLayout {
500 id: actionsLayout
501
502 LayoutMirroring.enabled: listItem.mirrored
503 anchors {
504 right: parent.right
505 top: parent.top
506 bottom: parent.bottom
507 rightMargin: Kirigami.Units.smallSpacing
508 }
509 visible: parent !== listItem
510 parent: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode
511 ? listItem.swipe.leftItem?.contentItem || listItem.swipe.rightItem?.contentItem || listItem
512 : overlayLoader
513
514 property bool hasVisibleActions: false
515 property int indexInListView: index ?? -1 // might not be set if using required properties
516
517 function updateVisibleActions(definitelyVisible: bool) {
518 hasVisibleActions = definitelyVisible || listItem.actions.some(isActionVisible);
519 }
520
521 function isActionVisible(action: T.Action): bool {
522 return (action instanceof Kirigami.Action) ? action.visible : true;
523 }
524
525 Repeater {
526 id: actionsRep
527 model: listItem.actions
528
529 delegate: QQC2.ToolButton {
530 required property T.Action modelData
531 required property int index
532
533 property bool tabbedFromDelegate: false
534
535 action: modelData
536 display: T.AbstractButton.IconOnly
537 visible: actionsLayout.isActionVisible(action)
538
539 onVisibleChanged: actionsLayout.updateVisibleActions(visible);
540 Component.onCompleted: actionsLayout.updateVisibleActions(visible);
541 Component.onDestruction: actionsLayout.updateVisibleActions(visible);
542
543 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
544 QQC2.ToolTip.visible: (Kirigami.Settings.tabletMode ? pressed : hovered) && QQC2.ToolTip.text.length > 0
545 QQC2.ToolTip.text: (action as Kirigami.Action)?.tooltip ?? action?.text ?? ""
546
547 onClicked: {
548 slideAnim.to = 0;
549 slideAnim.restart();
550 }
551
552 Keys.onBacktabPressed: (event) => {
553 if (tabbedFromDelegate) {
554 listItem.forceActiveFocus(Qt.BacktabFocusReason)
555 } else {
556 event.accepted = false
557 }
558 }
559
560 Keys.onPressed: (event) => {
561 if ((Qt.application.layoutDirection == Qt.LeftToRight && event.key == Qt.Key_Left) ||
562 (Qt.application.layoutDirection == Qt.RightToLeft && event.key == Qt.Key_Right)) {
563 for (var target = index -1; target>=0; target--) {
564 if (target == -1 || actionsLayout.children[target].visible) {
565 break
566 }
567 }
568 if (target == -1) {
569 listItem.forceActiveFocus(Qt.BacktabFocusReason)
570 } else {
571 actionsLayout.children[target].tabbedFromDelegate = tabbedFromDelegate
572 actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason)
573 }
574 event.accepted = true
575 } else if ((Qt.application.layoutDirection == Qt.LeftToRight && event.key == Qt.Key_Right) ||
576 (Qt.application.layoutDirection == Qt.RightToLeft && event.key == Qt.Key_Left)) {
577 var found=false
578 for (var target = index +1; target<actionsRep.count; target++) {
579 if (actionsLayout.children[target].visible) {
580 break
581 }
582 }
583 if (target < (actionsRep.count)) {
584 actionsLayout.children[target].tabbedFromDelegate = tabbedFromDelegate
585 actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason)
586 event.accepted = true
587 }
588 }
589 }
590
591 Keys.onUpPressed: (event) => {
592 if (listview && actionsLayout.indexInListView >= 0) {
593 listView.currentIndex = actionsLayout.indexInListView
594 }
595 event.accepted = false // pass to ListView
596 }
597
598 Keys.onDownPressed: (event) => {
599 if (listView && actionsLayout.indexInListView >= 0) {
600 listView.currentIndex = actionsLayout.indexInListView
601 }
602 event.accepted = false // pass to ListView
603 }
604
605 onActiveFocusChanged: {
606 if (focus && listView) {
607 listView.positionViewAtIndex(actionsLayout.indexInListView, ListView.Contain)
608 } else if (!focus) {
609 tabbedFromDelegate = false
610 }
611 }
612
613 Accessible.name: text
614 Accessible.description: (action as Kirigami.Action)?.tooltip ?? ""
615 }
616 }
617 }
618
619 swipe {
620 enabled: false
621 right: listItem.alwaysVisibleActions || listItem.mirrored || !Kirigami.Settings.tabletMode ? null : actionsBackgroundDelegate
622 left: listItem.alwaysVisibleActions || listItem.mirrored && Kirigami.Settings.tabletMode ? actionsBackgroundDelegate : null
623 }
624 NumberAnimation {
625 id: slideAnim
626 duration: Kirigami.Units.longDuration
627 easing.type: Easing.InOutQuad
628 target: listItem.swipe
629 property: "position"
630 from: listItem.swipe.position
631 }
632//END items
633
634 Component.onCompleted: {
635 listView: {
636 for (var targetItem = listItem; (targetItem.ListView.view === null); targetItem = targetItem.parent) {
637 }
638 listView = targetItem.ListView.view
639 }
640 }
641}
An item that represents an abstract Action.
Definition Action.qml:17
color textColor
This property holds the color of the text in the item.
alias supportsMouseEvents
This property sets whether the item should emit signals related to mouse interaction.
bool separatorVisible
This property sets whether the separator is visible.
color activeBackgroundColor
This property holds the color of the background when the item is pressed or selected.
bool alwaysVisibleActions
This property sets whether actions behind this SwipeListItem will always be visible.
color activeTextColor
This property holds the color of the text when the item is pressed or selected.
bool alternatingBackground
This property sets whether instances of this list item will alternate between two colors,...
color alternateBackgroundColor
This property holds the background color to be used when background alternating is enabled.
alias containsMouse
This property tells whether the cursor is currently hovering over the item.
color backgroundColor
This property holds the background color of the list item.
alias overlayWidth
This property holds the width of the overlay.
bool sectionDelegate
This property sets whether this item is a section delegate.
bool actionsVisible
This property tells whether actions are visible and interactive.
listTAction actions
This property holds actions of the list item.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
KGuiItem remove()
KGuiItem close()
KEDUVOCDOCUMENT_EXPORT QStringList languages()
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:47:53 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.