Merge lp:~ahayzen/music-app/remix-add-multi-select-mode into lp:music-app/remix

Proposed by Andrew Hayzen
Status: Merged
Approved by: Victor Thompson
Approved revision: 681
Merged at revision: 689
Proposed branch: lp:~ahayzen/music-app/remix-add-multi-select-mode
Merge into: lp:music-app/remix
Diff against target: 1307 lines (+669/-182)
9 files modified
MusicNowPlaying.qml (+151/-69)
MusicSearch.qml (+25/-0)
MusicTracks.qml (+91/-7)
MusicaddtoPlaylist.qml (+4/-5)
common/ListItemActions/AddToPlaylist.qml (+1/-1)
common/ListItemActions/CheckBox.qml (+25/-0)
common/ListItemWithActions.qml (+261/-86)
common/SongsPage.qml (+104/-10)
music-app.qml (+7/-4)
To merge this branch: bzr merge lp:~ahayzen/music-app/remix-add-multi-select-mode
Reviewer Review Type Date Requested Status
Ubuntu Phone Apps Jenkins Bot continuous-integration Approve
Victor Thompson Approve
Review via email: mp+239098@code.launchpad.net

Commit message

* Pull upstream ListItemWithActions.qml
* Add support for multiselect mode
* Disable triggerOnRelease

Description of the change

* Pull upstream ListItemWithActions.qml
* Add support for multiselect mode
* Disable triggerOnRelease

To post a comment you must log in.
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Victor Thompson (vthompson) wrote :

A few initial things need fixing:
1. It seems as though the playlists are filtered again while reordering, this causes the view to flicker and the checked items get confused.
2. After reordering selected items and adding them to a playlist from the queue the items aren't quite right. Perhaps we shouldn't allow reordering when items are being selected.

review: Needs Fixing
Revision history for this message
Victor Thompson (vthompson) wrote :

Also, we may want to talk to design about the "select all"/"select none" icon. Perhaps it could/should toggle to an empty square or a "clear" icon when all is selected?

Revision history for this message
Andrew Hayzen (ahayzen) wrote :

1) Fixed due to the change in 2)
2) Disabled reordering when selectmode is enabled

Revision history for this message
Victor Thompson (vthompson) wrote :

While I think preventing reorder when items are selected is the proper way of doing this... we probably should talk to Design about it.

Revision history for this message
Victor Thompson (vthompson) wrote :

Could we please have 2 signals...

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Victor Thompson (vthompson) wrote :

LGTM! This should help a lot and put us on the path towards improving performance elsewhere! Thanks!

review: Approve
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'MusicNowPlaying.qml'
2--- MusicNowPlaying.qml 2014-10-21 16:22:20 +0000
3+++ MusicNowPlaying.qml 2014-10-21 21:53:07 +0000
4@@ -25,6 +25,7 @@
5 import "common"
6 import "common/ListItemActions"
7 import "settings.js" as Settings
8+import "playlists.js" as Playlists
9
10 MusicPage {
11 id: nowPlaying
12@@ -41,43 +42,110 @@
13 }
14 }
15
16- head.backAction: Action {
17- iconName: "back";
18- objectName: "backButton"
19- onTriggered: {
20- mainPageStack.pop();
21-
22- while (mainPageStack.depth > 1) { // jump back to the tab layer if via SongsPage
23- mainPageStack.pop();
24- }
25- }
26- }
27-
28- head {
29- actions: [
30- Action {
31- objectName: "clearQueue"
32- iconName: "delete"
33- visible: isListView
34- onTriggered: {
35- head.backAction.trigger()
36- trackQueue.model.clear()
37- }
38- },
39- Action {
40- objectName: "toggleView"
41- iconName: "media-playlist"
42- onTriggered: {
43- isListView = !isListView
44- }
45- }
46- ]
47- }
48-
49 function positionAt(index) {
50 queuelist.positionViewAtIndex(index, ListView.Center);
51 }
52
53+ state: isListView && queuelist.state === "multiselectable" ? "selection" : "default"
54+ states: [
55+ PageHeadState {
56+ id: defaultState
57+
58+ name: "default"
59+ backAction: Action {
60+ iconName: "back";
61+ objectName: "backButton"
62+ onTriggered: {
63+ mainPageStack.pop();
64+
65+ while (mainPageStack.depth > 1) { // jump back to the tab layer if via SongsPage
66+ mainPageStack.pop();
67+ }
68+ }
69+ }
70+ actions: [
71+ Action {
72+ objectName: "clearQueue"
73+ iconName: "delete"
74+ visible: isListView
75+ onTriggered: {
76+ head.backAction.trigger()
77+ trackQueue.model.clear()
78+ }
79+ },
80+ Action {
81+ objectName: "toggleView"
82+ iconName: "media-playlist"
83+ onTriggered: {
84+ isListView = !isListView
85+ }
86+ }
87+ ]
88+ PropertyChanges {
89+ target: nowPlaying.head
90+ backAction: defaultState.backAction
91+ actions: defaultState.actions
92+ }
93+ },
94+ PageHeadState {
95+ id: selectionState
96+
97+ name: "selection"
98+ backAction: Action {
99+ text: i18n.tr("Cancel selection")
100+ iconName: "back"
101+ onTriggered: queuelist.state = "normal"
102+ }
103+ actions: [
104+ Action {
105+ text: i18n.tr("Select All")
106+ iconName: "select"
107+ onTriggered: {
108+ if (queuelist.selectedItems.length === queuelist.model.count) {
109+ queuelist.clearSelection()
110+ } else {
111+ queuelist.selectAll()
112+ }
113+ }
114+ },
115+ Action {
116+ enabled: queuelist.selectedItems.length > 0
117+ iconName: "add-to-playlist"
118+ text: i18n.tr("Add to playlist")
119+ onTriggered: {
120+ var items = []
121+
122+ for (var i=0; i < queuelist.selectedItems.length; i++) {
123+ items.push(makeDict(trackQueue.model.get(queuelist.selectedItems[i])));
124+ }
125+
126+ chosenElements = items;
127+ mainPageStack.push(addtoPlaylist)
128+
129+ queuelist.closeSelection()
130+ }
131+ },
132+ Action {
133+ enabled: queuelist.selectedItems.length > 0
134+ iconName: "delete"
135+ text: i18n.tr("Delete")
136+ onTriggered: {
137+ for (var i=0; i < queuelist.selectedItems.length; i++) {
138+ removeQueue(queuelist.selectedItems[i])
139+ }
140+
141+ queuelist.closeSelection()
142+ }
143+ }
144+ ]
145+ PropertyChanges {
146+ target: nowPlaying.head
147+ backAction: selectionState.backAction
148+ actions: selectionState.actions
149+ }
150+ }
151+ ]
152+
153 Rectangle {
154 id: fullview
155 anchors.fill: parent
156@@ -376,6 +444,25 @@
157 }
158 }
159
160+ function removeQueue(index)
161+ {
162+ var removedIndex = index
163+
164+ if (queuelist.count === 1) {
165+ player.stop()
166+ musicToolbar.goBack()
167+ } else if (index === player.currentIndex) {
168+ player.nextSong(player.isPlaying);
169+ }
170+
171+ queuelist.model.remove(index);
172+
173+ if (removedIndex < player.currentIndex) {
174+ // update index as the old has been removed
175+ player.currentIndex -= 1;
176+ }
177+ }
178+
179 ListView {
180 id: queuelist
181 anchors {
182@@ -388,23 +475,6 @@
183 }
184 model: trackQueue.model
185 objectName: "nowPlayingQueueList"
186- state: "normal"
187- states: [
188- State {
189- name: "normal"
190- PropertyChanges {
191- target: queuelist
192- interactive: true
193- }
194- },
195- State {
196- name: "reorder"
197- PropertyChanges {
198- target: queuelist
199- interactive: false
200- }
201- }
202- ]
203 visible: isListView
204
205 property int normalHeight: units.gu(6)
206@@ -414,6 +484,35 @@
207 customdebug("Queue: Now has: " + queuelist.count + " tracks")
208 }
209
210+ // Requirements for ListItemWithActions
211+ property var selectedItems: []
212+
213+ signal clearSelection()
214+ signal closeSelection()
215+ signal selectAll()
216+
217+ onClearSelection: selectedItems = []
218+ onCloseSelection: {
219+ clearSelection()
220+ state = "normal"
221+ }
222+ onSelectAll: {
223+ var tmp = selectedItems
224+
225+ for (var i=0; i < model.count; i++) {
226+ if (tmp.indexOf(i) === -1) {
227+ tmp.push(i)
228+ }
229+ }
230+
231+ selectedItems = tmp
232+ }
233+ onVisibleChanged: {
234+ if (!visible) {
235+ clearSelection(true)
236+ }
237+ }
238+
239 Component {
240 id: queueDelegate
241 ListItemWithActions {
242@@ -421,35 +520,18 @@
243 color: player.currentIndex === index ? "#2c2c34" : "transparent"
244 height: queuelist.normalHeight
245 objectName: "nowPlayingListItem" + index
246- showDivider: false
247 state: ""
248
249 leftSideAction: Remove {
250- onTriggered: {
251- var removedIndex = index
252-
253- if (queuelist.count === 1) {
254- player.stop()
255- musicToolbar.goBack()
256- } else if (index === player.currentIndex) {
257- player.nextSong(player.isPlaying);
258- }
259-
260- queuelist.model.remove(index);
261-
262- if (removedIndex < player.currentIndex) {
263- // update index as the old has been removed
264- player.currentIndex -= 1;
265- }
266- }
267+ onTriggered: removeQueue(index)
268 }
269+ multiselectable: true
270 reorderable: true
271 rightSideActions: [
272 AddToPlaylist{
273
274 }
275 ]
276- triggerActionOnMouseRelease: true
277
278 onItemClicked: {
279 customdebug("File: " + model.filename) // debugger
280
281=== modified file 'MusicSearch.qml'
282--- MusicSearch.qml 2014-09-20 15:41:33 +0000
283+++ MusicSearch.qml 2014-10-21 21:53:07 +0000
284@@ -154,6 +154,31 @@
285 searchTrackView.forceActiveFocus()
286 }
287
288+ // Requirements for ListItemWithActions
289+ property var selectedItems: []
290+
291+ signal clearSelection(bool closeSelection)
292+ signal selectAll()
293+
294+ onClearSelection: {
295+ selectedItems = []
296+
297+ if (closeSelection || closeSelection === undefined) {
298+ state = "normal"
299+ }
300+ }
301+ onSelectAll: {
302+ for (var i=0; i < model.count; i++) {
303+ if (selectedItems.indexOf(i) === -1) {
304+ selectedItems.push(i)
305+ }
306+ }
307+ }
308+ onVisibleChanged: {
309+ if (!visible) {
310+ clearSelection(true)
311+ }
312+ }
313 delegate: ListItemWithActions {
314 id: search
315 color: "transparent"
316
317=== modified file 'MusicTracks.qml'
318--- MusicTracks.qml 2014-10-10 05:50:32 +0000
319+++ MusicTracks.qml 2014-10-21 21:53:07 +0000
320@@ -34,6 +34,66 @@
321 objectName: "tracksPage"
322 title: i18n.tr("Songs")
323
324+ state: tracklist.state === "multiselectable" ? "selection" : "default"
325+ states: [
326+ PageHeadState {
327+ id: selectionState
328+ name: "selection"
329+ backAction: Action {
330+ text: i18n.tr("Cancel selection")
331+ iconName: "back"
332+ onTriggered: tracklist.state = "normal"
333+ }
334+ actions: [
335+ Action {
336+ iconName: "select"
337+ text: i18n.tr("Select All")
338+ onTriggered: {
339+ if (tracklist.selectedItems.length === tracklist.model.count) {
340+ tracklist.clearSelection()
341+ } else {
342+ tracklist.selectAll()
343+ }
344+ }
345+ },
346+ Action {
347+ enabled: tracklist.selectedItems.length !== 0
348+ iconName: "add-to-playlist"
349+ text: i18n.tr("Add to playlist")
350+ onTriggered: {
351+ var items = []
352+
353+ for (var i=0; i < tracklist.selectedItems.length; i++) {
354+ items.push(makeDict(tracklist.model.get(tracklist.selectedItems[i], tracklist.model.RoleModelData)));
355+ }
356+
357+ chosenElements = items;
358+ mainPageStack.push(addtoPlaylist)
359+
360+ tracklist.closeSelection()
361+ }
362+ },
363+ Action {
364+ enabled: tracklist.selectedItems.length > 0
365+ iconName: "add"
366+ text: i18n.tr("Add to queue")
367+ onTriggered: {
368+ for (var i=0; i < tracklist.selectedItems.length; i++) {
369+ trackQueue.model.append(makeDict(tracklist.model.get(tracklist.selectedItems[i], tracklist.model.RoleModelData)));
370+ }
371+
372+ tracklist.closeSelection()
373+ }
374+ }
375+ ]
376+ PropertyChanges {
377+ target: mainpage.head
378+ backAction: selectionState.backAction
379+ actions: selectionState.actions
380+ }
381+ }
382+ ]
383+
384 ListView {
385 id: tracklist
386 anchors {
387@@ -52,18 +112,46 @@
388 sort.property: "title"
389 sort.order: Qt.AscendingOrder
390 }
391+
392+ // Requirements for ListItemWithActions
393+ property var selectedItems: []
394+
395+ signal clearSelection()
396+ signal closeSelection()
397+ signal selectAll()
398+
399+ onClearSelection: selectedItems = []
400+ onCloseSelection: {
401+ clearSelection()
402+ state = "normal"
403+ }
404+ onSelectAll: {
405+ var tmp = selectedItems
406+
407+ for (var i=0; i < model.count; i++) {
408+ if (tmp.indexOf(i) === -1) {
409+ tmp.push(i)
410+ }
411+ }
412+
413+ selectedItems = tmp
414+ }
415+ onVisibleChanged: {
416+ if (!visible) {
417+ clearSelection(true)
418+ }
419+ }
420+
421 delegate: trackDelegate
422 Component {
423 id: trackDelegate
424
425 ListItemWithActions {
426 id: track
427- color: "transparent"
428 objectName: "tracksPageListItem" + index
429- width: parent.width
430 height: units.gu(7)
431- showDivider: false
432
433+ multiselectable: true
434 rightSideActions: [
435 AddToQueue {
436 },
437@@ -71,10 +159,6 @@
438
439 }
440 ]
441- triggerActionOnMouseRelease: true
442-
443- // TODO: If http://pad.lv/1354753 is fixed to expose whether the Shape should appear pressed, update this as well.
444- onPressedChanged: musicRow.pressed = pressed
445
446 onItemClicked: trackClicked(tracklist.model, index) // play track
447
448
449=== modified file 'MusicaddtoPlaylist.qml'
450--- MusicaddtoPlaylist.qml 2014-09-30 15:18:25 +0000
451+++ MusicaddtoPlaylist.qml 2014-10-21 21:53:07 +0000
452@@ -81,18 +81,17 @@
453 property string count: model.count
454
455 onClicked: {
456- console.debug("Debug: "+chosenElement.filename+" added to "+name)
457+ for (var i=0; i < chosenElements.length; i++) {
458+ console.debug("Debug: "+chosenElements[i].filename+" added to "+name)
459
460- Playlists.addToPlaylist(name, chosenElement)
461+ Playlists.addToPlaylist(name, chosenElements[i])
462+ }
463
464 playlistModel.filterPlaylists();
465
466 musicToolbar.goBack(); // go back to the previous page
467 }
468
469- // TODO: If http://pad.lv/1354753 is fixed to expose whether the Shape should appear pressed, update this as well.
470- onPressedChanged: musicRow.pressed = pressed
471-
472 MusicRow {
473 id: musicRow
474 covers: Playlists.getPlaylistCovers(playlist.name)
475
476=== modified file 'common/ListItemActions/AddToPlaylist.qml'
477--- common/ListItemActions/AddToPlaylist.qml 2014-09-20 10:50:45 +0000
478+++ common/ListItemActions/AddToPlaylist.qml 2014-10-21 21:53:07 +0000
479@@ -27,7 +27,7 @@
480 property bool primed: false
481
482 onTriggered: {
483- chosenElement = makeDict(model);
484+ chosenElements = [makeDict(model)];
485 console.debug("Debug: Add track to playlist");
486 mainPageStack.push(addtoPlaylist)
487 }
488
489=== added file 'common/ListItemActions/CheckBox.qml'
490--- common/ListItemActions/CheckBox.qml 1970-01-01 00:00:00 +0000
491+++ common/ListItemActions/CheckBox.qml 2014-10-21 21:53:07 +0000
492@@ -0,0 +1,25 @@
493+/*
494+ * Copyright (C) 2012-2014 Canonical, Ltd.
495+ *
496+ * This program is free software; you can redistribute it and/or modify
497+ * it under the terms of the GNU General Public License as published by
498+ * the Free Software Foundation; version 3.
499+ *
500+ * This program is distributed in the hope that it will be useful,
501+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
502+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
503+ * GNU General Public License for more details.
504+ *
505+ * You should have received a copy of the GNU General Public License
506+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
507+ */
508+
509+import QtQuick 2.2
510+import Ubuntu.Components 1.1
511+
512+CheckBox {
513+ checked: root.selected
514+ width: implicitWidth
515+ // disable item mouse area to avoid conflicts with parent mouse area
516+ __mouseArea.enabled: false
517+}
518
519=== modified file 'common/ListItemWithActions.qml'
520--- common/ListItemWithActions.qml 2014-09-20 10:50:45 +0000
521+++ common/ListItemWithActions.qml 2014-10-21 21:53:07 +0000
522@@ -19,116 +19,170 @@
523 import Ubuntu.Components.ListItems 1.0 as ListItem
524
525
526-ListItem.Standard { // CUSTOM
527-//Item {
528+Item {
529 id: root
530+ width: parent.width
531
532 property Action leftSideAction: null
533 property list<Action> rightSideActions
534 property double defaultHeight: units.gu(8)
535 property bool locked: false
536- property bool pressed: false
537 property Action activeAction: null
538 property var activeItem: null
539 property bool triggerActionOnMouseRelease: false
540- property alias color: main.color
541- default property alias contents: main.children
542-
543- property bool reorderable: false // CUSTOM
544- property bool reordering: false // CUSTOM
545-
546- readonly property double actionWidth: units.gu(5)
547+ property color color: Theme.palette.normal.background
548+ property color selectedColor: "#3d3d45" // "#E6E6E6" // CUSTOM
549+ property bool selected: false
550+ property bool selectionMode: false
551+ property alias internalAnchors: mainContents.anchors
552+ default property alias contents: mainContents.children
553+
554+ readonly property double actionWidth: units.gu(4) // CUSTOM 5?
555 readonly property double leftActionWidth: units.gu(10)
556 readonly property double actionThreshold: actionWidth * 0.4
557 readonly property double threshold: 0.4
558 readonly property string swipeState: main.x == 0 ? "Normal" : main.x > 0 ? "LeftToRight" : "RightToLeft"
559 readonly property alias swipping: mainItemMoving.running
560+ readonly property bool _showActions: mouseArea.pressed || swipeState != "Normal" || swipping
561+
562+ property bool reorderable: false // CUSTOM
563+ property bool reordering: false // CUSTOM
564+ property bool multiselectable: false // CUSTOM
565+
566+ property int previousListItemIndex: -1 // CUSTOM
567+ property int listItemIndex: index // CUSTOM
568+
569+ /* internal */
570+ property var _visibleRightSideActions: filterVisibleActions(rightSideActions)
571
572 signal itemClicked(var mouse)
573 signal itemPressAndHold(var mouse)
574
575 signal reorder(int from, int to) // CUSTOM
576
577- onItemPressAndHold: reordering = reorderable && !reordering // CUSTOM
578- onReorderingChanged: { // CUSTOM
579- if (reordering) {
580+ onItemPressAndHold: {
581+ //reordering = reorderable && !reordering // CUSTOM
582+ if (multiselectable) {
583+ selectionMode = true
584+ }
585+ }
586+ onListItemIndexChanged: {
587+ var i = parent.parent.selectedItems.lastIndexOf(previousListItemIndex)
588+
589+ if (i !== -1) {
590+ parent.parent.selectedItems[i] = listItemIndex
591+ }
592+
593+ previousListItemIndex = listItemIndex
594+ }
595+
596+ onSelectedChanged: {
597+ if (selectionMode) {
598+ var tmp = parent.parent.selectedItems
599+
600+ if (selected) {
601+ if (parent.parent.selectedItems.indexOf(listItemIndex) === -1) {
602+ tmp.push(listItemIndex)
603+ parent.parent.selectedItems = tmp
604+ }
605+ } else {
606+ tmp.splice(parent.parent.selectedItems.indexOf(listItemIndex), 1)
607+ parent.parent.selectedItems = tmp
608+ }
609+ }
610+ }
611+
612+ onSelectionModeChanged: { // CUSTOM
613+ if (reorderable && selectionMode) {
614 resetSwipe()
615 }
616
617 for (var j=0; j < main.children.length; j++) {
618- main.children[j].anchors.rightMargin = reordering ? actionReorder.width + units.gu(2) : 0
619- }
620-
621- parent.state = reordering ? "reorder" : "normal"
622+ main.children[j].anchors.rightMargin = reorderable && selectionMode ? actionReorder.width + units.gu(2) : 0
623+ }
624+
625+ parent.parent.state = selectionMode ? "multiselectable" : "normal"
626+
627+ if (!selectionMode) {
628+ selected = false
629+ }
630 }
631
632- function returnToBoundsRTL()
633+ function returnToBoundsRTL(direction)
634 {
635 var actionFullWidth = actionWidth + units.gu(2)
636+
637+ // go back to normal state if swipping reverse
638+ if (direction === "LTR") {
639+ updatePosition(0)
640+ return
641+ } else if (!triggerActionOnMouseRelease) {
642+ updatePosition(-rightActionsView.width + units.gu(2))
643+ return
644+ }
645+
646 var xOffset = Math.abs(main.x)
647- var index = Math.min(Math.floor(xOffset / actionFullWidth), rightSideActions.length)
648- var j; // CUSTOM
649-
650- if (index < 1) {
651- main.x = 0
652-
653- resetPrimed() // CUSTOM
654- } else if (index === rightSideActions.length) {
655- main.x = -rightActionsView.width
656+ var index = Math.min(Math.floor(xOffset / actionFullWidth), _visibleRightSideActions.length)
657+ var newX = 0
658+ var j // CUSTOM
659+
660+ if (index === _visibleRightSideActions.length) {
661+ newX = -(rightActionsView.width - units.gu(2))
662
663 for (j=0; j < rightSideActions.length; j++) { // CUSTOM
664 rightActionsRepeater.itemAt(j).primed = true
665 }
666- } else {
667- main.x = -(actionFullWidth * index)
668+ } else if (index >= 1) {
669+ newX = -(actionFullWidth * index)
670
671 for (j=0; j < rightSideActions.length; j++) { // CUSTOM
672 rightActionsRepeater.itemAt(j).primed = j === index
673 }
674 }
675+
676+ updatePosition(newX)
677 }
678
679- function returnToBoundsLTR()
680+ function returnToBoundsLTR(direction)
681 {
682 var finalX = leftActionWidth
683- if (main.x > (finalX * root.threshold))
684- main.x = finalX
685- else {
686- main.x = 0
687-
688- resetPrimed() // CUSTOM
689- }
690+ if ((direction === "RTL") || (main.x <= (finalX * root.threshold)))
691+ finalX = 0
692+ updatePosition(finalX)
693
694 if (leftSideAction !== null) { // CUSTOM
695 leftActionIcon.primed = main.x > (finalX * root.threshold)
696 }
697 }
698
699- function returnToBounds()
700+ function returnToBounds(direction)
701 {
702 if (main.x < 0) {
703- returnToBoundsRTL()
704+ returnToBoundsRTL(direction)
705 } else if (main.x > 0) {
706- returnToBoundsLTR()
707- } else { // CUSTOM
708- resetPrimed() // CUSTOM
709+ returnToBoundsLTR(direction)
710+ } else {
711+ updatePosition(0)
712 }
713 }
714
715- function contains(item, point)
716+ function contains(item, point, marginX)
717 {
718- return (point.x >= item.x) && (point.x <= (item.x + item.width)) && (point.y >= item.y) && (point.y <= (item.y + item.height));
719+ var itemStartX = item.x - marginX
720+ var itemEndX = item.x + item.width + marginX
721+ return (point.x >= itemStartX) && (point.x <= itemEndX) &&
722+ (point.y >= item.y) && (point.y <= (item.y + item.height));
723 }
724
725 function getActionAt(point)
726 {
727- if (contains(leftActionView, point)) {
728+ if (contains(leftActionView, point, 0)) {
729 return leftSideAction
730- } else if (contains(rightActionsView, point)) {
731+ } else if (contains(rightActionsView, point, 0)) {
732 var newPoint = root.mapToItem(rightActionsView, point.x, point.y)
733 for (var i = 0; i < rightActionsRepeater.count; i++) {
734 var child = rightActionsRepeater.itemAt(i)
735- if (contains(child, newPoint)) {
736+ if (contains(child, newPoint, units.gu(1))) {
737 return i
738 }
739 }
740@@ -138,15 +192,16 @@
741
742 function updateActiveAction()
743 {
744- if ((main.x <= -root.actionWidth) &&
745- (main.x > -rightActionsView.width)) {
746+ if (triggerActionOnMouseRelease &&
747+ (main.x <= -(root.actionWidth + units.gu(2))) &&
748+ (main.x > -(rightActionsView.width - units.gu(2)))) {
749 var actionFullWidth = actionWidth + units.gu(2)
750 var xOffset = Math.abs(main.x)
751- var index = Math.min(Math.floor(xOffset / actionFullWidth), rightSideActions.length)
752+ var index = Math.min(Math.floor(xOffset / actionFullWidth), _visibleRightSideActions.length)
753 index = index - 1
754 if (index > -1) {
755 root.activeItem = rightActionsRepeater.itemAt(index)
756- root.activeAction = root.rightSideActions[index]
757+ root.activeAction = root._visibleRightSideActions[index]
758 }
759 } else {
760 root.activeAction = null
761@@ -160,15 +215,40 @@
762 }
763
764 for (var j=0; j < rightSideActions.length; j++) {
765+ console.debug(rightActionsRepeater.itemAt(j));
766 rightActionsRepeater.itemAt(j).primed = false
767 }
768 }
769
770 function resetSwipe()
771 {
772- main.x = 0
773-
774- resetPrimed() // CUSTOM
775+ updatePosition(0)
776+ }
777+
778+ function filterVisibleActions(actions)
779+ {
780+ var visibleActions = []
781+ for(var i = 0; i < actions.length; i++) {
782+ var action = actions[i]
783+ if (action.visible) {
784+ visibleActions.push(action)
785+ }
786+ }
787+ return visibleActions
788+ }
789+
790+ function updatePosition(pos)
791+ {
792+ if (!root.triggerActionOnMouseRelease && (pos !== 0)) {
793+ mouseArea.state = pos > 0 ? "RightToLeft" : "LeftToRight"
794+ } else {
795+ mouseArea.state = ""
796+ }
797+ main.x = pos
798+
799+ if (pos === 0) { // CUSTOM
800+ //resetPrimed()
801+ }
802 }
803
804 Connections { // CUSTOM
805@@ -181,8 +261,10 @@
806 }
807
808 Connections { // CUSTOM
809- target: root.parent
810- onStateChanged: reordering = root.parent.state === "reorder"
811+ target: root.parent.parent
812+ onClearSelection: selected = false
813+ onSelectAll: selected = true
814+ onStateChanged: selectionMode = root.parent.parent.state === "multiselectable"
815 onVisibleChanged: {
816 if (!visible) {
817 reordering = false
818@@ -190,7 +272,13 @@
819 }
820 }
821
822- Component.onCompleted: reordering = root.parent.state === "reorder" // CUSTOM
823+ Component.onCompleted: { // CUSTOM
824+ if (parent.parent.selectedItems.indexOf(index) !== -1) { // FIXME:
825+ selected = true
826+ }
827+
828+ selectionMode = root.parent.parent.state === "multiselectable"
829+ }
830
831 /* CUSTOM Dim Component */
832 Rectangle {
833@@ -228,6 +316,26 @@
834 }
835 }
836
837+ states: [
838+ State {
839+ name: "select"
840+ when: selectionMode || selected
841+ PropertyChanges {
842+ target: selectionIcon
843+ source: Qt.resolvedUrl("ListItemActions/CheckBox.qml")
844+ anchors.leftMargin: units.gu(2)
845+ }
846+ PropertyChanges {
847+ target: root
848+ locked: true
849+ }
850+ PropertyChanges {
851+ target: main
852+ x: 0
853+ }
854+ }
855+ ]
856+
857 height: defaultHeight
858 clip: height !== defaultHeight
859
860@@ -241,16 +349,15 @@
861 }
862 width: root.leftActionWidth + actionThreshold
863 visible: leftSideAction
864- color: "red"
865+ color: UbuntuColors.red
866
867 Icon {
868- id: leftActionIcon
869 anchors {
870 centerIn: parent
871 horizontalCenterOffset: actionThreshold / 2
872 }
873 objectName: "swipeDeleteAction" // CUSTOM
874- name: leftSideAction ? leftSideAction.iconName : ""
875+ name: leftSideAction && _showActions ? leftSideAction.iconName : ""
876 color: Theme.palette.selected.field
877 height: units.gu(3)
878 width: units.gu(3)
879@@ -259,7 +366,8 @@
880 }
881 }
882
883- Item {
884+ //Rectangle {
885+ Item { // CUSTOM
886 id: rightActionsView
887
888 anchors {
889@@ -268,8 +376,9 @@
890 leftMargin: reordering ? actionReorder.width : units.gu(1) // CUSTOM
891 bottom: main.bottom
892 }
893- visible: rightSideActions.length > 0
894- width: rightActionsRepeater.count > 0 ? rightActionsRepeater.count * (root.actionWidth + units.gu(2)) + actionThreshold : 0
895+ visible: _visibleRightSideActions.length > 0
896+ width: rightActionsRepeater.count > 0 ? rightActionsRepeater.count * (root.actionWidth + units.gu(2)) + root.actionThreshold + units.gu(2) : 0
897+ // color: "white" // CUSTOM
898
899 Rectangle { // CUSTOM
900 anchors {
901@@ -283,22 +392,23 @@
902 }
903
904 Row {
905- anchors {
906- fill: parent
907- leftMargin: units.gu(2) // CUSTOM
908+ anchors{
909+ top: parent.top
910+ left: parent.left
911+ leftMargin: units.gu(2)
912+ right: parent.right
913+ rightMargin: units.gu(2)
914+ bottom: parent.bottom
915 }
916 spacing: units.gu(2)
917 Repeater {
918 id: rightActionsRepeater
919
920- model: rightSideActions
921+ model: _showActions ? _visibleRightSideActions : []
922 Item {
923 property alias image: img
924
925- anchors {
926- top: parent.top
927- bottom: parent.bottom
928- }
929+ height: rightActionsView.height
930 width: root.actionWidth
931
932 property alias primed: img.primed // CUSTOM
933@@ -310,8 +420,8 @@
934 objectName: rightSideActions[index].objectName // CUSTOM
935 width: units.gu(3)
936 height: units.gu(3)
937- name: iconName
938- color: root.activeAction === modelData || !root.triggerActionOnMouseRelease ? UbuntuColors.orange : styleMusic.common.white // CUSTOM
939+ name: modelData.iconName
940+ color: root.activeAction === modelData ? UbuntuColors.orange : styleMusic.common.white // CUSTOM
941
942 property bool primed: false // CUSTOM
943 }
944@@ -330,6 +440,38 @@
945 }
946
947 width: parent.width
948+ color: root.selected ? root.selectedColor : root.color
949+
950+ Loader {
951+ id: selectionIcon
952+
953+ anchors {
954+ left: main.left
955+ verticalCenter: main.verticalCenter
956+ }
957+ width: (status === Loader.Ready) ? item.implicitWidth : 0
958+ visible: (status === Loader.Ready) && (item.width === item.implicitWidth)
959+ Behavior on width {
960+ NumberAnimation {
961+ duration: UbuntuAnimation.SnapDuration
962+ }
963+ }
964+ }
965+
966+ Item {
967+ id: mainContents
968+
969+ anchors {
970+ left: selectionIcon.right
971+ //leftMargin: units.gu(2) // CUSTOM
972+ top: parent.top
973+ //topMargin: units.gu(1) // CUSTOM
974+ right: parent.right
975+ //rightMargin: units.gu(2) // CUSTOM
976+ bottom: parent.bottom
977+ //bottomMargin: units.gu(1) // CUSTOM
978+ }
979+ }
980
981 Behavior on x {
982 UbuntuNumberAnimation {
983@@ -342,7 +484,7 @@
984 }
985
986 /* CUSTOM Reorder Component */
987- Rectangle {
988+ Item {
989 id: actionReorder
990 anchors {
991 bottom: parent.bottom
992@@ -350,9 +492,8 @@
993 rightMargin: units.gu(1)
994 top: parent.top
995 }
996- color: "transparent"
997 width: units.gu(4)
998- visible: reordering
999+ visible: reorderable && selectionMode && root.parent.parent.selectedItems.length === 0
1000
1001 Icon {
1002 anchors {
1003@@ -469,7 +610,10 @@
1004 value: 1.0
1005 }
1006 ScriptAction {
1007- script: root.activeAction.triggered(root)
1008+ script: {
1009+ root.activeAction.triggered(root)
1010+ mouseArea.state = ""
1011+ }
1012 }
1013 PauseAnimation {
1014 duration: 500
1015@@ -479,23 +623,53 @@
1016 property: "x"
1017 to: 0
1018 }
1019- ScriptAction {
1020- script: resetPrimed()
1021- }
1022 }
1023
1024 MouseArea {
1025 id: mouseArea
1026
1027- property bool locked: root.locked || ((root.leftSideAction === null) && (root.rightSideActions.count === 0)) || reordering // CUSTOM
1028+ property bool locked: root.locked || ((root.leftSideAction === null) && (root._visibleRightSideActions.count === 0)) || reordering // CUSTOM
1029 property bool manual: false
1030+ property string direction: "None"
1031+ property real lastX: -1
1032
1033 anchors.fill: parent
1034 drag {
1035 target: locked ? null : main
1036 axis: Drag.XAxis
1037- minimumX: rightActionsView.visible ? -(rightActionsView.width + root.actionThreshold) : 0
1038+ minimumX: rightActionsView.visible ? -(rightActionsView.width) : 0
1039 maximumX: leftActionView.visible ? leftActionView.width : 0
1040+ threshold: root.actionThreshold
1041+ }
1042+
1043+ states: [
1044+ State {
1045+ name: "LeftToRight"
1046+ PropertyChanges {
1047+ target: mouseArea
1048+ drag.maximumX: 0
1049+ }
1050+ },
1051+ State {
1052+ name: "RightToLeft"
1053+ PropertyChanges {
1054+ target: mouseArea
1055+ drag.minimumX: 0
1056+ }
1057+ }
1058+ ]
1059+
1060+ onMouseXChanged: {
1061+ var offset = (lastX - mouseX)
1062+ if (Math.abs(offset) <= root.actionThreshold) {
1063+ return
1064+ }
1065+ lastX = mouseX
1066+ direction = offset > 0 ? "RTL" : "LTR";
1067+ }
1068+
1069+ onPressed: {
1070+ lastX = mouse.x
1071 }
1072
1073 onReleased: {
1074@@ -505,10 +679,13 @@
1075 root.returnToBounds()
1076 root.activeAction = null
1077 }
1078+ lastX = -1
1079+ direction = "None"
1080 }
1081 onClicked: {
1082- if (reordering) { // CUSTOM
1083- reordering = false
1084+ if (selectionMode) { // CUSTOM
1085+ selected = !selected
1086+ return
1087 }
1088 else if (main.x === 0) {
1089 root.itemClicked(mouse)
1090@@ -544,8 +721,6 @@
1091 }
1092 }
1093
1094- onPressedChanged: root.pressed = pressed
1095-
1096 z: -1
1097 }
1098 }
1099
1100=== modified file 'common/SongsPage.qml'
1101--- common/SongsPage.qml 2014-10-21 17:30:51 +0000
1102+++ common/SongsPage.qml 2014-10-21 21:53:07 +0000
1103@@ -44,7 +44,7 @@
1104 property alias album: songsModel.album
1105 property alias genre: songsModel.genre
1106
1107- state: songStackPage.line1 === i18n.tr("Playlist") ? "playlist" : "album"
1108+ state: albumtrackslist.state === "multiselectable" ? "selection" : (songStackPage.line1 === i18n.tr("Playlist") ? "playlist" : "album")
1109 states: [
1110 PageHeadState {
1111 id: albumState
1112@@ -57,7 +57,6 @@
1113 },
1114 PageHeadState {
1115 id: playlistState
1116-
1117 name: "playlist"
1118 actions: [
1119 Action {
1120@@ -82,6 +81,78 @@
1121 backAction: playlistState.backAction
1122 actions: playlistState.actions
1123 }
1124+ },
1125+ PageHeadState {
1126+ id: selectionState
1127+ name: "selection"
1128+ backAction: Action {
1129+ text: i18n.tr("Cancel selection")
1130+ iconName: "back"
1131+ onTriggered: albumtrackslist.state = "normal"
1132+ }
1133+ actions: [
1134+ Action {
1135+ iconName: "select"
1136+ text: i18n.tr("Select All")
1137+ onTriggered: {
1138+ if (albumtrackslist.selectedItems.length === albumtrackslist.model.count) {
1139+ albumtrackslist.clearSelection()
1140+ } else {
1141+ albumtrackslist.selectAll()
1142+ }
1143+ }
1144+ },
1145+ Action {
1146+ enabled: albumtrackslist.selectedItems.length > 0
1147+ iconName: "add-to-playlist"
1148+ text: i18n.tr("Add to playlist")
1149+ onTriggered: {
1150+ var items = []
1151+
1152+ for (var i=0; i < albumtrackslist.selectedItems.length; i++) {
1153+ items.push(makeDict(albumtrackslist.model.get(albumtrackslist.selectedItems[i], albumtrackslist.model.RoleModelData)));
1154+ }
1155+
1156+ chosenElements = items;
1157+ mainPageStack.push(addtoPlaylist)
1158+
1159+ albumtrackslist.closeSelection()
1160+ }
1161+ },
1162+ Action {
1163+ enabled: albumtrackslist.selectedItems.length > 0
1164+ iconName: "add"
1165+ text: i18n.tr("Add to queue")
1166+ onTriggered: {
1167+ for (var i=0; i < albumtrackslist.selectedItems.length; i++) {
1168+ trackQueue.model.append(makeDict(albumtrackslist.model.get(albumtrackslist.selectedItems[i], albumtrackslist.model.RoleModelData)));
1169+ }
1170+
1171+ albumtrackslist.closeSelection()
1172+ }
1173+ },
1174+ Action {
1175+ enabled: albumtrackslist.selectedItems.length > 0
1176+ iconName: "delete"
1177+ text: i18n.tr("Delete")
1178+ visible: songStackPage.line1 === i18n.tr("Playlist")
1179+ onTriggered: {
1180+ for (var i=0; i < albumtrackslist.selectedItems.length; i++) {
1181+ Playlists.removeFromPlaylist(songStackPage.line2, albumtrackslist.selectedItems[i] - i)
1182+ }
1183+
1184+ albumtrackslist.closeSelection()
1185+
1186+ albumTracksModel.filterPlaylistTracks(songStackPage.line2)
1187+ playlistModel.filterPlaylists()
1188+ }
1189+ }
1190+ ]
1191+ PropertyChanges {
1192+ target: songStackPage.head
1193+ backAction: selectionState.backAction
1194+ actions: selectionState.actions
1195+ }
1196 }
1197 ]
1198
1199@@ -99,6 +170,36 @@
1200 model: isAlbum ? songsModel : albumTracksModel.model
1201 objectName: "songspage-listview"
1202 width: parent.width
1203+
1204+ // Requirements for ListItemWithActions
1205+ property var selectedItems: []
1206+
1207+ signal clearSelection()
1208+ signal closeSelection()
1209+ signal selectAll()
1210+
1211+ onClearSelection: selectedItems = []
1212+ onCloseSelection: {
1213+ clearSelection()
1214+ state = "normal"
1215+ }
1216+ onSelectAll: {
1217+ var tmp = selectedItems
1218+
1219+ for (var i=0; i < model.count; i++) {
1220+ if (tmp.indexOf(i) === -1) {
1221+ tmp.push(i)
1222+ }
1223+ }
1224+
1225+ selectedItems = tmp
1226+ }
1227+ onVisibleChanged: {
1228+ if (!visible) {
1229+ clearSelection(true)
1230+ }
1231+ }
1232+
1233 header: BlurredHeader {
1234 rightColumn: Column {
1235 spacing: units.gu(2)
1236@@ -238,15 +339,12 @@
1237
1238 ListItemWithActions {
1239 id: track
1240- color: "transparent"
1241 objectName: "songsPageListItem" + index
1242- iconFrame: false
1243- progression: false
1244- showDivider: false
1245 height: units.gu(6)
1246
1247 leftSideAction: songStackPage.line1 === i18n.tr("Playlist")
1248 ? playlistRemoveAction.item : null
1249+ multiselectable: true
1250 reorderable: songStackPage.line1 === i18n.tr("Playlist")
1251 rightSideActions: [
1252 AddToQueue {
1253@@ -256,7 +354,6 @@
1254
1255 }
1256 ]
1257- triggerActionOnMouseRelease: true
1258
1259 onItemClicked: {
1260 trackClicked(albumtrackslist.model, index) // play track
1261@@ -289,9 +386,6 @@
1262 }
1263 }
1264
1265- // TODO: If http://pad.lv/1354753 is fixed to expose whether the Shape should appear pressed, update this as well.
1266- onPressedChanged: musicRow.pressed = pressed
1267-
1268 MusicRow {
1269 id: musicRow
1270 covers: []
1271
1272=== modified file 'music-app.qml'
1273--- music-app.qml 2014-10-21 14:59:51 +0000
1274+++ music-app.qml 2014-10-21 21:53:07 +0000
1275@@ -594,7 +594,7 @@
1276 property string lastfmusername
1277 property string lastfmpassword
1278 property string timestamp // used to scrobble
1279- property var chosenElement: null
1280+ property var chosenElements: []
1281 property bool toolbarShown: musicToolbar.visible
1282 property bool selectedAlbum: false
1283
1284@@ -875,7 +875,7 @@
1285 }
1286
1287 // Popover for tracks, queue and add to playlist, for example
1288- Component {
1289+ Component { // TODO: needed anymore? remove?
1290 id: trackPopoverComponent
1291 Popover {
1292 id: trackPopover
1293@@ -895,9 +895,12 @@
1294 anchors.verticalCenter: parent.verticalCenter
1295 }
1296 onClicked: {
1297- console.debug("Debug: Add track to queue: " + JSON.stringify(chosenElement))
1298+ console.debug("Debug: Add track to queue: " + JSON.stringify(chosenElements))
1299 PopupUtils.close(trackPopover)
1300- trackQueue.append(chosenElement)
1301+
1302+ for (var i=0; i < chosenElements.length; i++) {
1303+ trackQueue.append(chosenElements[i])
1304+ }
1305 }
1306 }
1307 ListItem.Standard {

Subscribers

People subscribed via source and target branches