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 LayoutMirroring.childrenInherit: true
193
194 hoverEnabled: true
195 implicitWidth: contentItem ? implicitContentWidth : Kirigami.Units.gridUnit * 12
196 width: parent ? parent.width : implicitWidth
197 implicitHeight: Math.max(Kirigami.Units.gridUnit * 2, implicitContentHeight) + topPadding + bottomPadding
198
199 padding: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode ? Kirigami.Units.largeSpacing : Kirigami.Units.smallSpacing
200
201 leftPadding: padding * 2 + (mirrored ? overlayLoader.paddingOffset : 0)
202 rightPadding: padding * 2 + (mirrored ? 0 : overlayLoader.paddingOffset)
203
204 topPadding: padding
205 bottomPadding: padding
206
207 Keys.onTabPressed: (event) => {
208 if (actionsLayout.hasVisibleActions) {
209 actionsLayout.children[0].tabbedFromDelegate = true
210 actionsLayout.children[0].forceActiveFocus(Qt.TabFocusReason)
211 } else {
212 event.accepted = false
213 }
214 }
215
216 Keys.onPressed: (event) => {
217 if ((actionsLayout.hasVisibleActions && activeFocus && event.key == Qt.Key_Right && Qt.application.layoutDirection == Qt.LeftToRight) ||
218 (actionsLayout.hasVisibleActions && activeFocus && event.key == Qt.Key_Left && Qt.application.layoutDirection == Qt.RightToLeft)) {
219 for (var target = 0; target < actionsRep.count; target ++) {
220 if (actionsLayout.children[target].visible) {
221 break
222 }
223 }
224 if (target < actionsRep.count) {
225 actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason)
226 event.accepted = true
227 }
228 }
229 }
230
231 QtObject {
232 id: internal
233
234 property Flickable view: listItem.ListView.view || (listItem.parent ? (listItem.parent.ListView.view || (listItem.parent instanceof Flickable ? listItem.parent : null)) : null)
235
236 function viewHasPropertySwipeFilter(): bool {
237 return view && view.parent && view.parent.parent && "_swipeFilter" in view.parent.parent;
238 }
239
240 readonly property QtObject swipeFilterItem: (viewHasPropertySwipeFilter() && view.parent.parent._swipeFilter) ? view.parent.parent._swipeFilter : null
241
242 readonly property bool edgeEnabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem || swipeFilterItem.currentItem === listItem.parent : false
243
244 // install the SwipeItemEventFilter
245 onViewChanged: {
246 if (listItem.alwaysVisibleActions || !Kirigami.Settings.tabletMode) {
247 return;
248 }
249 if (viewHasPropertySwipeFilter() && Kirigami.Settings.tabletMode && !internal.view.parent.parent._swipeFilter) {
250 const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml"));
251 internal.view.parent.parent._swipeFilter = component.createObject(internal.view.parent.parent);
252 component.destroy();
253 }
254 }
255 }
256
257 Connections {
258 target: Kirigami.Settings
259 function onTabletModeChanged() {
260 if (!internal.viewHasPropertySwipeFilter()) {
261 return;
262 }
263 if (Kirigami.Settings.tabletMode) {
264 if (!internal.swipeFilterItem) {
265 const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml"));
266 listItem.ListView.view.parent.parent._swipeFilter = component.createObject(listItem.ListView.view.parent.parent);
267 component.destroy();
268 }
269 } else {
270 if (listItem.ListView.view.parent.parent._swipeFilter) {
271 listItem.ListView.view.parent.parent._swipeFilter.destroy();
272 slideAnim.to = 0;
273 slideAnim.restart();
274 }
275 }
276 }
277 }
278
279//BEGIN items
280 Loader {
281 id: overlayLoader
282 readonly property int paddingOffset: (visible ? width : 0) + Kirigami.Units.smallSpacing
283 readonly property var theAlias: anchors
284 function validate(want, defaultValue) {
285 const expectedLeftPadding = () => listItem.padding * 2 + (listItem.mirrored ? overlayLoader.paddingOffset : 0)
286 const expectedRightPadding = () => listItem.padding * 2 + (listItem.mirrored ? 0 : overlayLoader.paddingOffset)
287
288 const warningText =
289 `Don't override the leftPadding or rightPadding on a SwipeListItem!\n` +
290 `This makes it impossible for me to adjust my layout as I need to for various usecases.\n` +
291 `I'll try to fix the mistake for you, but you should remove your overrides from your app's code entirely.\n` +
292 `If I can't fix the paddings, I'll fall back to a default layout, but it'll be slightly incorrect and lacks\n` +
293 `adaptations needed for touch screens and right-to-left languages, among other things.`
294
295 if (listItem.leftPadding != expectedLeftPadding() || listItem.rightPadding != expectedRightPadding()) {
296 listItem.leftPadding = Qt.binding(expectedLeftPadding)
297 listItem.rightPadding = Qt.binding(expectedRightPadding)
298 console.warn(warningText)
299 return defaultValue
300 }
301
302 return want
303 }
304 anchors {
305 right: validate(listItem.mirrored ? undefined : (contentItem ? contentItem.right : undefined), contentItem ? contentItem.right : undefined)
306 rightMargin: validate(-paddingOffset, 0)
307 left: validate(!listItem.mirrored ? undefined : (contentItem ? contentItem.left : undefined), undefined)
308 leftMargin: validate(-paddingOffset, 0)
309 top: parent.top
310 bottom: parent.bottom
311 }
312 LayoutMirroring.enabled: false
313
314 parent: listItem
315 z: contentItem ? contentItem.z + 1 : 0
316 width: item ? item.implicitWidth : actionsLayout.implicitWidth
317 active: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode
318 visible: listItem.actionsVisible && opacity > 0
319 asynchronous: true
320 sourceComponent: handleComponent
321 opacity: listItem.alwaysVisibleActions || Kirigami.Settings.tabletMode || listItem.hovered ? 1 : 0
322 Behavior on opacity {
323 OpacityAnimator {
324 id: opacityAnim
325 duration: Kirigami.Units.veryShortDuration
326 easing.type: Easing.InOutQuad
327 }
328 }
329 }
330
331 Component {
332 id: handleComponent
333
334 MouseArea {
335 id: dragButton
336 anchors {
337 right: parent.right
338 }
339 implicitWidth: Kirigami.Units.iconSizes.smallMedium
340
341 preventStealing: true
342 readonly property real openPosition: (listItem.width - width - listItem.leftPadding * 2)/listItem.width
343 property real startX: 0
344 property real lastPosition: 0
345 property bool openIntention
346
347 onPressed: mouse => {
348 startX = mapToItem(listItem, 0, 0).x;
349 }
350 onClicked: mouse => {
351 if (Math.abs(mapToItem(listItem, 0, 0).x - startX) > Qt.styleHints.startDragDistance) {
352 return;
353 }
354 if (listItem.mirrored) {
355 if (listItem.swipe.position < 0.5) {
356 slideAnim.to = openPosition
357 } else {
358 slideAnim.to = 0
359 }
360 } else {
361 if (listItem.swipe.position > -0.5) {
362 slideAnim.to = -openPosition
363 } else {
364 slideAnim.to = 0
365 }
366 }
367 slideAnim.restart();
368 }
369 onPositionChanged: mouse => {
370 const pos = mapToItem(listItem, mouse.x, mouse.y);
371
372 if (listItem.mirrored) {
373 listItem.swipe.position = Math.max(0, Math.min(openPosition, (pos.x / listItem.width)));
374 openIntention = listItem.swipe.position > lastPosition;
375 } else {
376 listItem.swipe.position = Math.min(0, Math.max(-openPosition, (pos.x / (listItem.width -listItem.rightPadding) - 1)));
377 openIntention = listItem.swipe.position < lastPosition;
378 }
379 lastPosition = listItem.swipe.position;
380 }
381 onReleased: mouse => {
382 if (listItem.mirrored) {
383 if (openIntention) {
384 slideAnim.to = openPosition
385 } else {
386 slideAnim.to = 0
387 }
388 } else {
389 if (openIntention) {
390 slideAnim.to = -openPosition
391 } else {
392 slideAnim.to = 0
393 }
394 }
395 slideAnim.restart();
396 }
397
398 Kirigami.Icon {
399 id: handleIcon
400 anchors.fill: parent
401 selected: listItem.checked || (listItem.down && !listItem.checked && !listItem.sectionDelegate)
402 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"))
403 }
404
405 Connections {
406 id: swipeFilterConnection
407
408 target: internal.edgeEnabled ? internal.swipeFilterItem : null
409 function onPeekChanged() {
410 if (!listItem.actionsVisible) {
411 return;
412 }
413
414 if (listItem.mirrored) {
415 listItem.swipe.position = Math.max(0, Math.min(dragButton.openPosition, internal.swipeFilterItem.peek));
416 dragButton.openIntention = listItem.swipe.position > dragButton.lastPosition;
417
418 } else {
419 listItem.swipe.position = Math.min(0, Math.max(-dragButton.openPosition, -internal.swipeFilterItem.peek));
420 dragButton.openIntention = listItem.swipe.position < dragButton.lastPosition;
421 }
422
423 dragButton.lastPosition = listItem.swipe.position;
424 }
425 function onPressed(mouse) {
426 if (internal.edgeEnabled) {
427 dragButton.pressed(mouse);
428 }
429 }
430 function onClicked(mouse) {
431 if (Math.abs(listItem.background.x) < Kirigami.Units.gridUnit && internal.edgeEnabled) {
432 dragButton.clicked(mouse);
433 }
434 }
435 function onReleased(mouse) {
436 if (internal.edgeEnabled) {
437 dragButton.released(mouse);
438 }
439 }
440 function onCurrentItemChanged() {
441 if (!internal.edgeEnabled) {
442 slideAnim.to = 0;
443 slideAnim.restart();
444 }
445 }
446 }
447 }
448 }
449
450 // TODO: expose in API?
451 Component {
452 id: actionsBackgroundDelegate
453 Item {
454 anchors.fill: parent
455 z: 1
456
457 readonly property Item contentItem: swipeBackground
458 Rectangle {
459 id: swipeBackground
460 anchors {
461 top: parent.top
462 bottom: parent.bottom
463 }
464 clip: true
465 color: parent.pressed ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Qt.darker(Kirigami.Theme.backgroundColor, 1.05)
466 x: listItem.mirrored ? listItem.background.x - width : (listItem.background.x + listItem.background.width)
467 width: listItem.mirrored ? parent.width - (parent.width - x) : parent.width - x
468
469 TapHandler {
470 onTapped: listItem.swipe.close()
471 }
472 EdgeShadow {
473 edge: Qt.TopEdge
474 visible: background.x != 0
475 anchors {
476 right: parent.right
477 left: parent.left
478 top: parent.top
479 }
480 }
481 EdgeShadow {
482 edge: listItem.mirrored ? Qt.RightEdge : Qt.LeftEdge
483
484 visible: background.x != 0
485 anchors {
486 top: parent.top
487 bottom: parent.bottom
488 }
489 }
490 }
491
492 visible: listItem.swipe.position != 0
493 }
494 }
495
496
497 RowLayout {
498 id: actionsLayout
499
500 LayoutMirroring.enabled: listItem.mirrored
501 anchors {
502 right: parent.right
503 top: parent.top
504 bottom: parent.bottom
505 rightMargin: Kirigami.Units.smallSpacing
506 }
507 visible: parent !== listItem
508 parent: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode
509 ? listItem.swipe.leftItem?.contentItem || listItem.swipe.rightItem?.contentItem || listItem
510 : overlayLoader
511
512 property bool hasVisibleActions: false
513 property int indexInListView: index ?? -1 // might not be set if using required properties
514
515 function updateVisibleActions(definitelyVisible: bool) {
516 hasVisibleActions = definitelyVisible || listItem.actions.some(isActionVisible);
517 }
518
519 function isActionVisible(action: T.Action): bool {
520 return (action instanceof Kirigami.Action) ? action.visible : true;
521 }
522
523 Repeater {
524 id: actionsRep
525 model: listItem.actions
526
527 delegate: QQC2.ToolButton {
528 required property T.Action modelData
529 required property int index
530
531 property bool tabbedFromDelegate: false
532
533 action: modelData
534 display: T.AbstractButton.IconOnly
535 visible: actionsLayout.isActionVisible(action)
536
537 onVisibleChanged: actionsLayout.updateVisibleActions(visible);
538 Component.onCompleted: actionsLayout.updateVisibleActions(visible);
539 Component.onDestruction: actionsLayout.updateVisibleActions(visible);
540
541 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
542 QQC2.ToolTip.visible: (Kirigami.Settings.tabletMode ? pressed : hovered) && QQC2.ToolTip.text.length > 0
543 QQC2.ToolTip.text: (action as Kirigami.Action)?.tooltip ?? action?.text ?? ""
544
545 onClicked: {
546 slideAnim.to = 0;
547 slideAnim.restart();
548 }
549
550 Keys.onBacktabPressed: (event) => {
551 if (tabbedFromDelegate) {
552 listItem.forceActiveFocus(Qt.BacktabFocusReason)
553 } else {
554 event.accepted = false
555 }
556 }
557
558 Keys.onPressed: (event) => {
559 if ((Qt.application.layoutDirection == Qt.LeftToRight && event.key == Qt.Key_Left) ||
560 (Qt.application.layoutDirection == Qt.RightToLeft && event.key == Qt.Key_Right)) {
561 for (var target = index -1; target>=0; target--) {
562 if (target == -1 || actionsLayout.children[target].visible) {
563 break
564 }
565 }
566 if (target == -1) {
567 listItem.forceActiveFocus(Qt.BacktabFocusReason)
568 } else {
569 actionsLayout.children[target].tabbedFromDelegate = tabbedFromDelegate
570 actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason)
571 }
572 event.accepted = true
573 } else if ((Qt.application.layoutDirection == Qt.LeftToRight && event.key == Qt.Key_Right) ||
574 (Qt.application.layoutDirection == Qt.RightToLeft && event.key == Qt.Key_Left)) {
575 var found=false
576 for (var target = index +1; target<actionsRep.count; target++) {
577 if (actionsLayout.children[target].visible) {
578 break
579 }
580 }
581 if (target < (actionsRep.count)) {
582 actionsLayout.children[target].tabbedFromDelegate = tabbedFromDelegate
583 actionsLayout.children[target].forceActiveFocus(Qt.TabFocusReason)
584 event.accepted = true
585 }
586 }
587 }
588
589 Keys.onUpPressed: (event) => {
590 if (actionsLayout.indexInListView >= 0) {
591 listItem.ListView.view.currentIndex = actionsLayout.indexInListView
592 }
593 event.accepted = false // pass to ListView
594 }
595
596 Keys.onDownPressed: (event) => {
597 if (actionsLayout.indexInListView >= 0) {
598 listItem.ListView.view.currentIndex = actionsLayout.indexInListView
599 }
600 event.accepted = false // pass to ListView
601 }
602
603 onActiveFocusChanged: {
604 if (focus) {
605 listItem.ListView.view.positionViewAtIndex(actionsLayout.indexInListView, ListView.Contain)
606 } else if (!focus) {
607 tabbedFromDelegate = false
608 }
609 }
610
611 Accessible.name: text
612 Accessible.description: (action as Kirigami.Action)?.tooltip ?? ""
613 }
614 }
615 }
616
617 swipe {
618 enabled: false
619 right: listItem.alwaysVisibleActions || listItem.mirrored || !Kirigami.Settings.tabletMode ? null : actionsBackgroundDelegate
620 left: listItem.alwaysVisibleActions || listItem.mirrored && Kirigami.Settings.tabletMode ? actionsBackgroundDelegate : null
621 }
622 NumberAnimation {
623 id: slideAnim
624 duration: Kirigami.Units.longDuration
625 easing.type: Easing.InOutQuad
626 target: listItem.swipe
627 property: "position"
628 from: listItem.swipe.position
629 }
630//END items
631}
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 Jan 24 2025 11:51:21 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.