Kirigami2

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

KDE's Doxygen guidelines are available online.