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

KDE's Doxygen guidelines are available online.