Dekko
Loading...
Searching...
No Matches
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
17import QtQuick 2.4
18import QtQuick.Controls.Suru 2.2
19import QtFeedback 5.0
20import Lomiri.Components 1.3
21import Lomiri.Components.ListItems 1.0 as ListItem
22import Dekko.Components 1.0
23import Dekko.Lomiri.Constants 1.0
24
25PixelPerfectItem {
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}
Definition main.qml:18