Dekko
ListItemWithActions.qml
1 /*
2  * Copyright (C) 2012-2014 Canonical, Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 import QtQuick 2.4
18 import QtQuick.Controls.Suru 2.2
19 import QtFeedback 5.0
20 import Lomiri.Components 1.3
21 import Lomiri.Components.ListItems 1.0 as ListItem
22 import Dekko.Components 1.0
23 import Dekko.Lomiri.Constants 1.0
24 
25 PixelPerfectItem {
26  id: root
27 
28  property Action leftSideAction: null
29  property list<Action> rightSideActions
30  property double defaultHeight: units.gu(8)
31  property bool locked: false
32  property Action activeAction: null
33  property int triggerIndex: -1
34  property var activeItem: null
35  property bool triggerActionOnMouseRelease: false
36  property bool selected: false
37  property bool selectionMode: false
38  property bool showActionHighlight: true
39  property alias showDivider: divider.visible
40  property alias internalAnchors: mainContents.anchors
41  default property alias contents: mainContents.children
42 
43  readonly property double actionWidth: units.gu(4)
44  readonly property double leftActionWidth: units.gu(10)
45  readonly property double actionThreshold: actionWidth * 0.4
46  readonly property double threshold: 0.4
47  readonly property string swipeState: main.x == 0 ? "Normal" : main.x > 0 ? "LeftToRight" : "RightToLeft"
48  readonly property alias swipping: mainItemMoving.running
49  readonly property bool _showActions: mouseArea.pressed || swipeState != "Normal" || swipping
50 
51  /* internal */
52  property var _visibleRightSideActions: filterVisibleActions(rightSideActions)
53  signal itemDoubleClicked(var mouse)
54  signal itemClicked(var mouse)
55  signal itemPressAndHold(var mouse)
56 
57  function returnToBoundsRTL()
58  {
59  var actionFullWidth = actionWidth + Style.defaultSpacing
60  var xOffset = Math.abs(main.x)
61  var index = Math.min(Math.floor(xOffset / actionFullWidth), _visibleRightSideActions.length)
62 
63  if (index < 1) {
64  main.x = 0
65  } else if (index === _visibleRightSideActions.length) {
66  main.x = -(rightActionsView.width - Style.defaultSpacing)
67  } else {
68  main.x = -(actionFullWidth * index)
69  }
70  }
71 
72  function returnToBoundsLTR()
73  {
74  var finalX = leftActionWidth
75  if (main.x > (finalX * root.threshold))
76  main.x = finalX
77  else
78  main.x = 0
79  }
80 
81  function returnToBounds()
82  {
83  if (main.x < 0) {
84  returnToBoundsRTL()
85  } else if (main.x > 0) {
86  returnToBoundsLTR()
87  }
88  }
89 
90  function contains(item, point, marginX)
91  {
92  var itemStartX = item.x - marginX
93  var itemEndX = item.x + item.width + marginX
94  return (point.x >= itemStartX) && (point.x <= itemEndX) &&
95  (point.y >= item.y) && (point.y <= (item.y + item.height));
96  }
97 
98  function getActionAt(point)
99  {
100  if (contains(leftActionView, point, 0)) {
101  return leftSideAction
102  } else if (contains(rightActionsView, point, 0)) {
103  var newPoint = root.mapToItem(rightActionsView, point.x, point.y)
104  for (var i = 0; i < rightActionsRepeater.count; i++) {
105  var child = rightActionsRepeater.itemAt(i)
106  if (contains(child, newPoint, units.gu(1))) {
107  return i
108  }
109  }
110  }
111  return -1
112  }
113 
114  function updateActiveAction()
115  {
116  if ((main.x <= -(root.actionWidth + Style.defaultSpacing)) &&
117  (main.x > -(rightActionsView.width - Style.defaultSpacing))) {
118  var actionFullWidth = actionWidth + Style.defaultSpacing
119  var xOffset = Math.abs(main.x)
120  var index = Math.min(Math.floor(xOffset / actionFullWidth), _visibleRightSideActions.length)
121  index = index - 1
122  if (index > -1) {
123  root.activeItem = rightActionsRepeater.itemAt(index)
124  root.activeAction = root._visibleRightSideActions[index]
125  }
126  } else {
127  root.activeAction = null
128  }
129  }
130 
131  function resetSwipe()
132  {
133  main.x = 0
134  }
135 
136  function filterVisibleActions(actions)
137  {
138  var visibleActions = []
139  for(var i = 0; i < actions.length; i++) {
140  var action = actions[i]
141  if (action.visible) {
142  visibleActions.push(action)
143  }
144  }
145  return visibleActions
146  }
147 
148  states: [
149  State {
150  name: "select"
151  when: selectionMode || selected
152  PropertyChanges {
153  target: root
154  locked: true
155  }
156  PropertyChanges {
157  target: main
158  x: 0
159  }
160  }
161  ]
162 
163  height: defaultHeight
164  clip: height !== defaultHeight
165  HapticsEffect {
166  id: clickEffect
167  attackIntensity: 0.0
168  attackTime: 50
169  intensity: 1.0
170  duration: 10
171  fadeTime: 50
172  fadeIntensity: 0.0
173  }
174 
175  Rectangle {
176  id: leftActionView
177 
178  anchors {
179  top: parent.top
180  bottom: parent.bottom
181  right: main.left
182  }
183  width: root.leftActionWidth + actionThreshold
184  visible: leftSideAction
185  Suru.highlightType: Suru.NegativeHighlight
186  color: Suru.highlightColor
187 
188  Icon {
189  anchors {
190  centerIn: parent
191  horizontalCenterOffset: actionThreshold / 2
192  }
193  name: leftSideAction && _showActions ? leftSideAction.iconName : ""
194  color: Theme.palette.selected.field
195  height: units.gu(3)
196  width: units.gu(3)
197  }
198  }
199 
200  PixelPerfectItem {
201  id: rightActionsView
202 
203  anchors {
204  top: main.top
205  left: main.right
206  bottom: main.bottom
207  }
208  visible: _visibleRightSideActions.length > 0
209  width: rightActionsRepeater.count > 0 ? rightActionsRepeater.count * (root.actionWidth + Style.defaultSpacing) + root.actionThreshold + Style.defaultSpacing : 0
210  Row {
211  anchors{
212  top: parent.top
213  left: parent.left
214  leftMargin: Style.defaultSpacing
215  right: parent.right
216  rightMargin: Style.defaultSpacing
217  bottom: parent.bottom
218  }
219  spacing: Style.defaultSpacing
220  Repeater {
221  id: rightActionsRepeater
222 
223  model: _showActions ? _visibleRightSideActions : []
224  PixelPerfectItem {
225  id: actItem
226  property alias image: img
227 
228  height: rightActionsView.height
229  width: root.actionWidth
230 
231  Icon {
232  id: img
233 
234  anchors.centerIn: parent
235  width: units.gu(3)
236  height: units.gu(3)
237  source: modelData.iconSource ? modelData.iconSource : null
238  name: modelData.iconName ? modelData.iconName : ""
239  color: root.activeAction === modelData || !root.triggerActionOnMouseRelease ? LomiriColors.orange : Suru.tertiaryForegroundColor
240  }
241  Rectangle {
242  id: underscore
243  width: actItem.width
244  height: units.gu(0.2)
245  anchors {
246  bottom: actItem.bottom
247  bottomMargin: units.gu(1.5)
248 
249  }
250  // Both this and the action icon should match the header color when active
251  color: LomiriColors.orange
252  visible: root.activeAction === modelData && root.showActionHighlight
253  onVisibleChanged: {
254  if (visible) {
255  clickEffect.start()
256  }
257  }
258  }
259  }
260  }
261  }
262  }
263 
264  PixelPerfectItem {
265  id: main
266  objectName: "mainItem"
267 
268  anchors {
269  top: parent.top
270  bottom: parent.bottom
271  }
272 
273  width: parent.width
274 
275  Loader {
276  id: selectionIcon
277 
278  anchors {
279  left: main.left
280  verticalCenter: main.verticalCenter
281  }
282  width: (status === Loader.Ready) ? item.implicitWidth : 0
283  visible: (status === Loader.Ready) && (item.width === item.implicitWidth)
284  Behavior on width {
285  NumberAnimation {
286  duration: LomiriAnimation.SnapDuration
287  }
288  }
289  }
290 
291  PixelPerfectItem {
292  id: mainContents
293  anchors.fill: parent
294  }
295 
296  Behavior on x {
297  LomiriNumberAnimation {
298  id: mainItemMoving
299 
300  easing.type: Easing.OutElastic
301  duration: LomiriAnimation.SlowDuration
302  }
303  }
304  }
305 
306  SequentialAnimation {
307  id: triggerAction
308 
309  property var currentItem: root.activeItem ? root.activeItem.image : null
310 
311  running: false
312  ParallelAnimation {
313  LomiriNumberAnimation {
314  target: triggerAction.currentItem
315  property: "opacity"
316  from: 1.0
317  to: 0.0
318  duration: LomiriAnimation.SlowDuration
319  easing {type: Easing.InOutBack; }
320  }
321  LomiriNumberAnimation {
322  target: triggerAction.currentItem
323  properties: "width, height"
324  from: units.gu(3)
325  to: root.actionWidth
326  duration: LomiriAnimation.SlowDuration
327  easing {type: Easing.InOutBack; }
328  }
329  }
330  PropertyAction {
331  target: triggerAction.currentItem
332  properties: "width, height"
333  value: units.gu(3)
334  }
335  PropertyAction {
336  target: triggerAction.currentItem
337  properties: "opacity"
338  value: 1.0
339  }
340  ScriptAction {
341  script: {
342  root.activeAction.triggered(root)
343  root.activeAction = null;
344  }
345  }
346  PauseAnimation {
347  duration: 500
348  }
349  LomiriNumberAnimation {
350  target: main
351  property: "x"
352  to: 0
353 
354  }
355  }
356 
357  MouseArea {
358  id: mouseArea
359 
360  property bool locked: root.locked || ((root.leftSideAction === null) && (root._visibleRightSideActions.count === 0))
361  property bool manual: false
362  acceptedButtons: Qt.LeftButton | Qt.RightButton
363  anchors {
364  top: parent.top
365  bottom: parent.bottom
366  right: parent.right
367  left: parent.left
368  leftMargin: mouseArea.drag.active ? units.gu(4) : units.gu(1.5)
369  rightMargin: units.gu(1.5)
370  }
371  drag {
372  target: locked ? null : main
373  axis: Drag.XAxis
374  minimumX: rightActionsView.visible ? -(rightActionsView.width) : 0
375  maximumX: leftActionView.visible ? leftActionView.width : 0
376  threshold: root.actionThreshold
377  }
378 
379  onReleased: {
380  // if the mouse reach the safe are we should handle it as full swipe
381  if (mouse.x < 0) {
382  main.x = -(rightActionsView.width - Style.defaultSpacing)
383  } else if (root.triggerActionOnMouseRelease && root.activeAction) {
384  clickEffect.start()
385  triggerAction.start()
386  } else {
387  root.returnToBounds()
388  root.activeAction = null
389  }
390  }
391  onClicked: {
392  if (mouse.button === Qt.RightButton) {
393  console.log("RIGHT BUTTON CLICKED")
394  }
395 
396  clickEffect.start()
397  if (main.x === 0) {
398  root.itemClicked(mouse)
399  } else if (main.x > 0) {
400  var action = getActionAt(Qt.point(mouse.x, mouse.y))
401  if (action && action !== -1) {
402  if (triggerIndex > -1) {
403  action.triggered(triggerIndex)
404  } else {
405  action.triggered(root)
406  }
407  }
408  } else {
409  var actionIndex = getActionAt(Qt.point(mouse.x, mouse.y))
410  if (actionIndex !== -1) {
411  root.activeItem = rightActionsRepeater.itemAt(actionIndex)
412  root.activeAction = root._visibleRightSideActions[actionIndex]
413  triggerAction.start()
414  return
415  }
416  }
417  root.resetSwipe()
418  }
419  onDoubleClicked: {
420  clickEffect.start()
421  if (main.x === 0) {
422  root.doubleClicked(mouse)
423  } else if (main.x > 0) {
424  var action = getActionAt(Qt.point(mouse.x, mouse.y))
425  if (action && action !== -1) {
426  action.triggered(root)
427  }
428  } else {
429  var actionIndex = getActionAt(Qt.point(mouse.x, mouse.y))
430  if (actionIndex !== -1) {
431  root.activeItem = rightActionsRepeater.itemAt(actionIndex)
432  root.activeAction = root._visibleRightSideActions[actionIndex]
433  triggerAction.start()
434  return
435  }
436  }
437  }
438 
439  onPositionChanged: {
440  if (mouseArea.pressed) {
441  updateActiveAction()
442  }
443  }
444  onPressAndHold: {
445  if (main.x === 0) {
446  root.itemPressAndHold(mouse)
447  }
448  }
449  z: -1
450  }
451 
452  ListItem.ThinDivider {
453  id: divider
454  visible: false
455  width: parent.width + units.gu(4)
456  anchors {
457  left: parent.left
458  right: parent.right
459  bottom: parent.bottom
460  }
461  }
462 }
main
Definition: main.qml:18
Dekko
Definition: Dekko.qml:30
Dekko::Lomiri::Constants::Style::defaultSpacing
int defaultSpacing
Definition: Style.qml:27
Dekko::Lomiri::Constants::Style
Definition: Style.qml:23