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 QtObject {
208 id: internal
209
210 property Flickable view: listItem.ListView.view || (listItem.parent ? (listItem.parent.ListView.view || (listItem.parent instanceof Flickable ? listItem.parent : null)) : null)
211
212 function viewHasPropertySwipeFilter(): bool {
213 return view && view.parent && view.parent.parent && "_swipeFilter" in view.parent.parent;
214 }
215
216 readonly property QtObject swipeFilterItem: (viewHasPropertySwipeFilter() && view.parent.parent._swipeFilter) ? view.parent.parent._swipeFilter : null
217
218 readonly property bool edgeEnabled: swipeFilterItem ? swipeFilterItem.currentItem === listItem || swipeFilterItem.currentItem === listItem.parent : false
219
220 // install the SwipeItemEventFilter
221 onViewChanged: {
222 if (listItem.alwaysVisibleActions || !Kirigami.Settings.tabletMode) {
223 return;
224 }
225 if (viewHasPropertySwipeFilter() && Kirigami.Settings.tabletMode && !internal.view.parent.parent._swipeFilter) {
226 const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml"));
227 internal.view.parent.parent._swipeFilter = component.createObject(internal.view.parent.parent);
228 component.destroy();
229 }
230 }
231 }
232
233 Connections {
234 target: Kirigami.Settings
235 function onTabletModeChanged() {
236 if (!internal.viewHasPropertySwipeFilter()) {
237 return;
238 }
239 if (Kirigami.Settings.tabletMode) {
240 if (!internal.swipeFilterItem) {
241 const component = Qt.createComponent(Qt.resolvedUrl("../private/SwipeItemEventFilter.qml"));
242 listItem.ListView.view.parent.parent._swipeFilter = component.createObject(listItem.ListView.view.parent.parent);
243 component.destroy();
244 }
245 } else {
246 if (listItem.ListView.view.parent.parent._swipeFilter) {
247 listItem.ListView.view.parent.parent._swipeFilter.destroy();
248 slideAnim.to = 0;
249 slideAnim.restart();
250 }
251 }
252 }
253 }
254
255//BEGIN items
256 Loader {
257 id: overlayLoader
258 readonly property int paddingOffset: (visible ? width : 0) + Kirigami.Units.smallSpacing
259 readonly property var theAlias: anchors
260 function validate(want, defaultValue) {
261 const expectedLeftPadding = () => listItem.padding * 2 + (listItem.mirrored ? overlayLoader.paddingOffset : 0)
262 const expectedRightPadding = () => listItem.padding * 2 + (listItem.mirrored ? 0 : overlayLoader.paddingOffset)
263
264 const warningText =
265 `Don't override the leftPadding or rightPadding on a SwipeListItem!\n` +
266 `This makes it impossible for me to adjust my layout as I need to for various usecases.\n` +
267 `I'll try to fix the mistake for you, but you should remove your overrides from your app's code entirely.\n` +
268 `If I can't fix the paddings, I'll fall back to a default layout, but it'll be slightly incorrect and lacks\n` +
269 `adaptations needed for touch screens and right-to-left languages, among other things.`
270
271 if (listItem.leftPadding != expectedLeftPadding() || listItem.rightPadding != expectedRightPadding()) {
272 listItem.leftPadding = Qt.binding(expectedLeftPadding)
273 listItem.rightPadding = Qt.binding(expectedRightPadding)
274 console.warn(warningText)
275 return defaultValue
276 }
277
278 return want
279 }
280 anchors {
281 right: validate(listItem.mirrored ? undefined : (contentItem ? contentItem.right : undefined), contentItem ? contentItem.right : undefined)
282 rightMargin: validate(-paddingOffset, 0)
283 left: validate(!listItem.mirrored ? undefined : (contentItem ? contentItem.left : undefined), undefined)
284 leftMargin: validate(-paddingOffset, 0)
285 top: parent.top
286 bottom: parent.bottom
287 }
288 LayoutMirroring.enabled: false
289
290 parent: listItem
291 z: contentItem ? contentItem.z + 1 : 0
292 width: item ? item.implicitWidth : actionsLayout.implicitWidth
293 active: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode
294 visible: listItem.actionsVisible && opacity > 0
295 asynchronous: true
296 sourceComponent: handleComponent
297 opacity: listItem.alwaysVisibleActions || Kirigami.Settings.tabletMode || listItem.hovered ? 1 : 0
298 Behavior on opacity {
299 OpacityAnimator {
300 id: opacityAnim
301 duration: Kirigami.Units.veryShortDuration
302 easing.type: Easing.InOutQuad
303 }
304 }
305 }
306
307 Component {
308 id: handleComponent
309
310 MouseArea {
311 id: dragButton
312 anchors {
313 right: parent.right
314 }
315 implicitWidth: Kirigami.Units.iconSizes.smallMedium
316
317 preventStealing: true
318 readonly property real openPosition: (listItem.width - width - listItem.leftPadding * 2)/listItem.width
319 property real startX: 0
320 property real lastPosition: 0
321 property bool openIntention
322
323 onPressed: mouse => {
324 startX = mapToItem(listItem, 0, 0).x;
325 }
326 onClicked: mouse => {
327 if (Math.abs(mapToItem(listItem, 0, 0).x - startX) > Qt.styleHints.startDragDistance) {
328 return;
329 }
330 if (listItem.mirrored) {
331 if (listItem.swipe.position < 0.5) {
332 slideAnim.to = openPosition
333 } else {
334 slideAnim.to = 0
335 }
336 } else {
337 if (listItem.swipe.position > -0.5) {
338 slideAnim.to = -openPosition
339 } else {
340 slideAnim.to = 0
341 }
342 }
343 slideAnim.restart();
344 }
345 onPositionChanged: mouse => {
346 const pos = mapToItem(listItem, mouse.x, mouse.y);
347
348 if (listItem.mirrored) {
349 listItem.swipe.position = Math.max(0, Math.min(openPosition, (pos.x / listItem.width)));
350 openIntention = listItem.swipe.position > lastPosition;
351 } else {
352 listItem.swipe.position = Math.min(0, Math.max(-openPosition, (pos.x / (listItem.width -listItem.rightPadding) - 1)));
353 openIntention = listItem.swipe.position < lastPosition;
354 }
355 lastPosition = listItem.swipe.position;
356 }
357 onReleased: mouse => {
358 if (listItem.mirrored) {
359 if (openIntention) {
360 slideAnim.to = openPosition
361 } else {
362 slideAnim.to = 0
363 }
364 } else {
365 if (openIntention) {
366 slideAnim.to = -openPosition
367 } else {
368 slideAnim.to = 0
369 }
370 }
371 slideAnim.restart();
372 }
373
374 Kirigami.Icon {
375 id: handleIcon
376 anchors.fill: parent
377 selected: listItem.checked || (listItem.down && !listItem.checked && !listItem.sectionDelegate)
378 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"))
379 }
380
381 Connections {
382 id: swipeFilterConnection
383
384 target: internal.edgeEnabled ? internal.swipeFilterItem : null
385 function onPeekChanged() {
386 if (!listItem.actionsVisible) {
387 return;
388 }
389
390 if (listItem.mirrored) {
391 listItem.swipe.position = Math.max(0, Math.min(dragButton.openPosition, internal.swipeFilterItem.peek));
392 dragButton.openIntention = listItem.swipe.position > dragButton.lastPosition;
393
394 } else {
395 listItem.swipe.position = Math.min(0, Math.max(-dragButton.openPosition, -internal.swipeFilterItem.peek));
396 dragButton.openIntention = listItem.swipe.position < dragButton.lastPosition;
397 }
398
399 dragButton.lastPosition = listItem.swipe.position;
400 }
401 function onPressed(mouse) {
402 if (internal.edgeEnabled) {
403 dragButton.pressed(mouse);
404 }
405 }
406 function onClicked(mouse) {
407 if (Math.abs(listItem.background.x) < Kirigami.Units.gridUnit && internal.edgeEnabled) {
408 dragButton.clicked(mouse);
409 }
410 }
411 function onReleased(mouse) {
412 if (internal.edgeEnabled) {
413 dragButton.released(mouse);
414 }
415 }
416 function onCurrentItemChanged() {
417 if (!internal.edgeEnabled) {
418 slideAnim.to = 0;
419 slideAnim.restart();
420 }
421 }
422 }
423 }
424 }
425
426 // TODO: expose in API?
427 Component {
428 id: actionsBackgroundDelegate
429 Item {
430 anchors.fill: parent
431 z: 1
432
433 readonly property Item contentItem: swipeBackground
434 Rectangle {
435 id: swipeBackground
436 anchors {
437 top: parent.top
438 bottom: parent.bottom
439 }
440 clip: true
441 color: parent.pressed ? Qt.darker(Kirigami.Theme.backgroundColor, 1.1) : Qt.darker(Kirigami.Theme.backgroundColor, 1.05)
442 x: listItem.mirrored ? listItem.background.x - width : (listItem.background.x + listItem.background.width)
443 width: listItem.mirrored ? parent.width - (parent.width - x) : parent.width - x
444
445 TapHandler {
446 onTapped: listItem.swipe.close()
447 }
448 EdgeShadow {
449 edge: Qt.TopEdge
450 visible: background.x != 0
451 anchors {
452 right: parent.right
453 left: parent.left
454 top: parent.top
455 }
456 }
457 EdgeShadow {
458 edge: listItem.mirrored ? Qt.RightEdge : Qt.LeftEdge
459
460 visible: background.x != 0
461 anchors {
462 top: parent.top
463 bottom: parent.bottom
464 }
465 }
466 }
467
468 visible: listItem.swipe.position != 0
469 }
470 }
471
472
473 RowLayout {
474 id: actionsLayout
475
476 LayoutMirroring.enabled: listItem.mirrored
477 anchors {
478 right: parent.right
479 top: parent.top
480 bottom: parent.bottom
481 rightMargin: Kirigami.Units.smallSpacing
482 }
483 visible: parent !== listItem
484 parent: !listItem.alwaysVisibleActions && Kirigami.Settings.tabletMode
485 ? listItem.swipe.leftItem?.contentItem || listItem.swipe.rightItem?.contentItem || listItem
486 : overlayLoader
487
488 property bool hasVisibleActions: false
489
490 function updateVisibleActions(definitelyVisible: bool) {
491 hasVisibleActions = definitelyVisible || listItem.actions.some(isActionVisible);
492 }
493
494 function isActionVisible(action: T.Action): bool {
495 return (action instanceof Kirigami.Action) ? action.visible : true;
496 }
497
498 Repeater {
499 model: listItem.actions
500
501 delegate: QQC2.ToolButton {
502 required property T.Action modelData
503
504 action: modelData
505 display: T.AbstractButton.IconOnly
506 visible: actionsLayout.isActionVisible(action)
507
508 onVisibleChanged: actionsLayout.updateVisibleActions(visible);
509 Component.onCompleted: actionsLayout.updateVisibleActions(visible);
510 Component.onDestruction: actionsLayout.updateVisibleActions(visible);
511
512 QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
513 QQC2.ToolTip.visible: (Kirigami.Settings.tabletMode ? pressed : hovered) && QQC2.ToolTip.text.length > 0
514 QQC2.ToolTip.text: (action as Kirigami.Action)?.tooltip ?? action?.text ?? ""
515
516 onClicked: {
517 slideAnim.to = 0;
518 slideAnim.restart();
519 }
520
521 Accessible.name: text
522 Accessible.description: (action as Kirigami.Action)?.tooltip ?? ""
523 }
524 }
525 }
526
527 swipe {
528 enabled: false
529 right: listItem.alwaysVisibleActions || listItem.mirrored || !Kirigami.Settings.tabletMode ? null : actionsBackgroundDelegate
530 left: listItem.alwaysVisibleActions || listItem.mirrored && Kirigami.Settings.tabletMode ? actionsBackgroundDelegate : null
531 }
532 NumberAnimation {
533 id: slideAnim
534 duration: Kirigami.Units.longDuration
535 easing.type: Easing.InOutQuad
536 target: listItem.swipe
537 property: "position"
538 from: listItem.swipe.position
539 }
540//END items
541}
An item that represents an abstract Action.
Definition Action.qml:17
KGuiItem remove()
const QList< QKeySequence > & close()
KEDUVOCDOCUMENT_EXPORT QStringList languages()
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 Dec 6 2024 12:09:31 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.