@@ -16,8 +16,10 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
19+import Ubuntu.Components.ListItems 0.1 as ListItems
1920import Unity 0.1
2021import Unity.Launcher 0.1
22+import Ubuntu.Components.Popups 0.1
2123import "../Components/ListItems"
2224
2325Item {
@@ -29,53 +31,33 @@ Item {
2931 property bool inverted: true
3032 property bool dragging: false
3133 property bool moving: launcherListView.moving || launcherListView.flicking
32− property int dragPosition: 0
34+ property bool preventHiding: moving || dndArea.draggedIndex >= 0 || dndArea.quickListPopover !== null || dndArea.pressed
3335 property int highlightIndex: -1
3436
3537 signal applicationSelected(string desktopFile)
3638 signal dashItemSelected(int index)
3739
38− onDragPositionChanged: {
39− var effectiveDragPosition = root.inverted ? launcherListView.height - dragPosition : dragPosition - mainColumn.anchors.margins
40−
41− var hiddenContentHeight = launcherListView.contentHeight - launcherListView.height
42− // Shortening scrollable height because the first/last item needs to be fully expanded before reaching the top/bottom
43− var scrollableHeight = launcherListView.height - (launcherListView.itemSize + launcherColumn.spacing) *2
44− // As we shortened the scrollableHeight, lets move everything down by the itemSize
45− var shortenedEffectiveDragPosition = effectiveDragPosition - launcherListView.itemSize - launcherColumn.spacing
46− var newContentY = shortenedEffectiveDragPosition * hiddenContentHeight / scrollableHeight
47−
48− // limit top/bottom to prevent overshooting
49− launcherListView.contentY = Math.min(hiddenContentHeight, Math.max(0, newContentY));
50−
51− // Now calculate the current index:
52− // > the current mouse position + the hidden/scolled content on top is the mouse position in the averall view
53− // > adjust that removing all the margins
54− // > divide by itemSize to get index
55− highlightIndex = (effectiveDragPosition + launcherListView.contentY - mainColumn.anchors.margins*3 - launcherColumn.spacing/2) / (launcherListView.itemSize + launcherColumn.spacing)
56− }
57−
5840 BorderImage {
5941 id: background
6042 source: "graphics/launcher_bg.sci"
6143 anchors.fill: parent
44+ anchors.rightMargin: root.inverted ? 0 : -units.gu(1)
45+ anchors.leftMargin: root.inverted ? -units.gu(1) : 0
46+ rotation: root.rotation
6247 }
6348
6449 Column {
6550 id: mainColumn
6651 anchors {
6752 fill: parent
68− topMargin: units.gu(0.5)
69− bottomMargin: units.gu(1)
7053 leftMargin: units.gu(0.5)
7154 rightMargin: units.gu(0.5)
7255 }
73− spacing: units.gu(0.5)
7456
7557 MouseArea {
7658 id: dashItem
7759 width: parent.width
78− height: units.gu(6.5)
60+ height: units.gu(7)
7961 onClicked: root.dashItemSelected(0)
8062 z: 1
8163 Image {
@@ -100,87 +82,392 @@ Item {
10082 anchors.left: parent.left
10183 anchors.right: parent.right
10284 height: parent.height - dashItem.height - parent.spacing*2
103− ListView {
104− id: launcherListView
105− objectName: "launcherListView"
85+
86+ Item {
10687 anchors.fill: parent
107− anchors.topMargin: -itemSize
108− anchors.bottomMargin: -itemSize
109− topMargin: itemSize
110− bottomMargin: itemSize
111− height: parent.height - dashItem.height - parent.spacing*2
112− model: root.model
113− cacheBuffer: itemSize * 3
114− snapMode: ListView.SnapToItem
115− highlightRangeMode: ListView.ApplyRange
116− preferredHighlightBegin: (height - itemSize) / 2
117− preferredHighlightEnd: (height + itemSize) / 2
118−
119− // The height of the area where icons start getting folded
120− property int foldingAreaHeight: itemSize * 0.75
121− property int itemSize: width
122− property int clickFlickSpeed: units.gu(60)
123−
124− delegate: LauncherDelegate {
125− id: launcherDelegate
126− objectName: "launcherDelegate" + index
127− width: launcherListView.itemSize
128− height: launcherListView.itemSize
129− iconName: model.icon
130− inverted: root.inverted
131− highlighted: root.dragging && index === root.highlightIndex
132− z: -Math.abs(offset)
133− state: "docked"
134− maxAngle: 60
135−
136− onClicked: {
137− // First/last item do the scrolling at more than 12 degrees
138− if (index == 0 || index == launcherListView.count -1) {
139− if (angle > 12) {
88+ clip: true
89+
90+ ListView {
91+ id: launcherListView
92+ objectName: "launcherListView"
93+ anchors {
94+ fill: parent
95+ topMargin: -extensionSize + units.gu(0.5)
96+ bottomMargin: -extensionSize + units.gu(1)
97+ }
98+ topMargin: extensionSize
99+ bottomMargin: extensionSize
100+ height: parent.height - dashItem.height - parent.spacing*2
101+ model: root.model
102+ cacheBuffer: itemHeight * 3
103+ snapMode: interactive ? ListView.SnapToItem : ListView.NoSnap
104+ highlightRangeMode: ListView.ApplyRange
105+ preferredHighlightBegin: (height - itemHeight) / 2
106+ preferredHighlightEnd: (height + itemHeight) / 2
107+ spacing: units.gu(0.5)
108+
109+ // The size of the area the ListView is extended to make sure items are not
110+ // destroyed when dragging them outside the list. This needs to be at least
111+ // itemHeight to prevent folded items from disappearing and DragArea limits
112+ // need to be smaller than this size to avoid breakage.
113+ property int extensionSize: itemHeight * 3
114+
115+ // The height of the area where icons start getting folded
116+ property int foldingAreaHeight: itemHeight * 0.75
117+ property int itemWidth: width
118+ property int itemHeight: width * 7.5 / 8
119+ property int clickFlickSpeed: units.gu(60)
120+ property int draggedIndex: dndArea.draggedIndex
121+ property real realContentY: contentY - originY + topMargin
122+ property int realItemHeight: itemHeight + spacing
123+
124+ displaced: Transition {
125+ NumberAnimation { properties: "x,y"; duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing }
126+ }
127+
128+ delegate: FoldingLauncherDelegate {
129+ id: launcherDelegate
130+ objectName: "launcherDelegate" + index
131+ itemHeight: launcherListView.itemHeight
132+ itemWidth: launcherListView.itemWidth
133+ width: itemWidth
134+ height: itemHeight
135+ iconName: model.icon
136+ count: model.count
137+ progress: model.progress
138+ inverted: root.inverted
139+ highlighted: dragging && index === root.highlightIndex
140+ z: -Math.abs(offset)
141+ maxAngle: 60
142+ property bool dragging: false
143+
144+ ThinDivider {
145+ id: dropIndicator
146+ objectName: "dropIndicator"
147+ anchors.centerIn: parent
148+ width: parent.width + mainColumn.anchors.leftMargin + mainColumn.anchors.rightMargin
149+ opacity: 0
150+ }
151+
152+ states: [
153+ State {
154+ name: "selected"
155+ when: dndArea.selectedItem === launcherDelegate && fakeDragItem.visible && !dragging
156+ PropertyChanges {
157+ target: launcherDelegate
158+ itemOpacity: 0
159+ }
160+ },
161+ State {
162+ name: "dragging"
163+ when: dragging
164+ PropertyChanges {
165+ target: launcherDelegate
166+ height: units.gu(1)
167+ itemOpacity: 0
168+ }
169+ PropertyChanges {
170+ target: dropIndicator
171+ opacity: 1
172+ }
173+ },
174+ State {
175+ name: "expanded"
176+ when: dndArea.draggedIndex >= 0 && (dndArea.preDragging || dndArea.dragging || dndArea.postDragging) && dndArea.draggedIndex != index
177+ PropertyChanges {
178+ target: launcherDelegate
179+ angle: 0
180+ offset: 0
181+ itemOpacity: 0.6
182+ }
183+ }
184+ ]
185+
186+ transitions: [
187+ Transition {
188+ from: ""
189+ to: "selected"
190+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
191+ },
192+ Transition {
193+ from: "*"
194+ to: "expanded"
195+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
196+ UbuntuNumberAnimation { properties: "angle,offset" }
197+ },
198+ Transition {
199+ from: "expanded"
200+ to: ""
201+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
202+ UbuntuNumberAnimation { properties: "angle,offset" }
203+ },
204+ Transition {
205+ from: "selected"
206+ to: "dragging"
207+ UbuntuNumberAnimation { properties: "height" }
208+ NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
209+ },
210+ Transition {
211+ from: "dragging"
212+ to: "*"
213+ NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
214+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
215+ SequentialAnimation {
216+ UbuntuNumberAnimation { properties: "height" }
217+ PropertyAction { target: dndArea; property: "postDragging"; value: false }
218+ PropertyAction { target: dndArea; property: "draggedIndex"; value: -1 }
219+ }
220+ }
221+ ]
222+ }
223+
224+ MouseArea {
225+ id: dndArea
226+ objectName: "dndArea"
227+ anchors {
228+ fill: parent
229+ topMargin: launcherListView.topMargin
230+ bottomMargin: launcherListView.bottomMargin
231+ }
232+ drag.minimumY: -launcherListView.topMargin
233+ drag.maximumY: height + launcherListView.bottomMargin
234+
235+ property int draggedIndex: -1
236+ property var selectedItem
237+ property bool preDragging: false
238+ property bool dragging: selectedItem !== undefined && selectedItem.dragging
239+ property bool postDragging: false
240+ property int startX
241+ property int startY
242+ property var quickListPopover: null
243+
244+ onPressed: {
245+ selectedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
246+ selectedItem.highlighted = true
247+ }
248+
249+ onClicked: {
250+ var index = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
251+ var clickedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
252+
253+ // First/last item do the scrolling at more than 12 degrees
254+ if (index == 0 || index == launcherListView.count - 1) {
255+ if (clickedItem.angle > 12) {
256+ launcherListView.flick(0, -launcherListView.clickFlickSpeed);
257+ } else if (clickedItem.angle < -12) {
258+ launcherListView.flick(0, launcherListView.clickFlickSpeed);
259+ } else {
260+ root.applicationSelected(LauncherModel.get(index).desktopFile);
261+ }
262+ return;
263+ }
264+
265+ // the rest launches apps up to an angle of 30 degrees
266+ if (clickedItem.angle > 30) {
140267 launcherListView.flick(0, -launcherListView.clickFlickSpeed);
141− } else if (angle < -12) {
268+ } else if (clickedItem.angle < -30) {
142269 launcherListView.flick(0, launcherListView.clickFlickSpeed);
143270 } else {
144271 root.applicationSelected(LauncherModel.get(index).desktopFile);
145272 }
146− return;
147273 }
148274
149− // the rest launches apps up to an angle of 30 degrees
150− if (angle > 30) {
151− launcherListView.flick(0, -launcherListView.clickFlickSpeed);
152− } else if (angle < -30) {
153− launcherListView.flick(0, launcherListView.clickFlickSpeed);
154− } else {
155− root.applicationSelected(LauncherModel.get(index).desktopFile);
275+ onCanceled: {
276+ selectedItem.highlighted = false
277+ selectedItem = undefined;
278+ preDragging = false;
279+ postDragging = false;
280+ }
281+
282+ onReleased: {
283+ var droppedIndex = draggedIndex;
284+ if (dragging) {
285+ postDragging = true;
286+ } else {
287+ draggedIndex = -1;
288+ }
289+
290+ selectedItem.highlighted = false
291+ selectedItem.dragging = false;
292+ selectedItem = undefined;
293+ preDragging = false;
294+
295+ drag.target = undefined
296+
297+ progressiveScrollingTimer.stop();
298+ launcherListView.interactive = true;
299+ if (droppedIndex >= launcherListView.count - 2 && postDragging) {
300+ launcherListView.flick(0, -launcherListView.clickFlickSpeed);
301+ }
302+ }
303+
304+ onPressAndHold: {
305+ if (Math.abs(selectedItem.angle) > 30) {
306+ return;
307+ }
308+
309+ draggedIndex = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
310+
311+ // Opening QuickList
312+ var quickListModel = launcherListView.model.get(draggedIndex).quickList
313+ var quickListAppId = launcherListView.model.get(draggedIndex).appId
314+
315+ quickListPopover = PopupUtils.open(popoverComponent, selectedItem,
316+ {model: quickListModel, appId: quickListAppId})
317+
318+ launcherListView.interactive = false
319+
320+ var yOffset = draggedIndex > 0 ? (mouseY + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouseY + launcherListView.realContentY
321+
322+ fakeDragItem.iconName = launcherListView.model.get(draggedIndex).icon
323+ fakeDragItem.x = 0
324+ fakeDragItem.y = mouseY - yOffset + launcherListView.anchors.topMargin + launcherListView.topMargin
325+ fakeDragItem.angle = selectedItem.angle * (root.inverted ? -1 : 1)
326+ fakeDragItem.offset = selectedItem.offset * (root.inverted ? -1 : 1)
327+ fakeDragItem.count = LauncherModel.get(draggedIndex).count
328+ fakeDragItem.progress = LauncherModel.get(draggedIndex).progress
329+ fakeDragItem.flatten()
330+ drag.target = fakeDragItem
331+
332+ startX = mouseX
333+ startY = mouseY
334+ }
335+
336+ onPositionChanged: {
337+ if (draggedIndex >= 0) {
338+ if (!selectedItem.dragging) {
339+ var distance = Math.max(Math.abs(mouseX - startX), Math.abs(mouseY - startY))
340+ if (!preDragging && distance > units.gu(1.5)) {
341+ preDragging = true;
342+ PopupUtils.close(quickListPopover)
343+ }
344+ if (distance > launcherListView.itemHeight) {
345+ selectedItem.dragging = true
346+ preDragging = false;
347+ }
348+ }
349+ if (!selectedItem.dragging) {
350+ return
351+ }
352+
353+ var itemCenterY = fakeDragItem.y + fakeDragItem.height / 2
354+
355+ // Move it down by the the missing size to compensate index calculation with only expanded items
356+ itemCenterY += (launcherListView.itemHeight - selectedItem.height) / 2
357+
358+ if (mouseY > launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin - launcherListView.realItemHeight) {
359+ progressiveScrollingTimer.downwards = false
360+ progressiveScrollingTimer.start()
361+ } else if (mouseY < launcherListView.realItemHeight) {
362+ progressiveScrollingTimer.downwards = true
363+ progressiveScrollingTimer.start()
364+ } else {
365+ progressiveScrollingTimer.stop()
366+ }
367+
368+ var newIndex = (itemCenterY + launcherListView.realContentY) / launcherListView.realItemHeight
369+
370+ if (newIndex > draggedIndex + 1) {
371+ newIndex = draggedIndex + 1
372+ } else if (newIndex < draggedIndex) {
373+ newIndex = draggedIndex -1
374+ } else {
375+ return
376+ }
377+
378+ if (newIndex >= 0 && newIndex < launcherListView.count) {
379+ launcherListView.model.move(draggedIndex, newIndex)
380+ draggedIndex = newIndex
381+ }
382+ }
383+ }
384+ }
385+ Timer {
386+ id: progressiveScrollingTimer
387+ interval: 2
388+ repeat: true
389+ running: false
390+ property bool downwards: true
391+ onTriggered: {
392+ if (downwards) {
393+ var minY = -launcherListView.topMargin
394+ if (launcherListView.contentY > minY) {
395+ launcherListView.contentY = Math.max(launcherListView.contentY - units.dp(2), minY)
396+ }
397+ } else {
398+ var maxY = launcherListView.contentHeight - launcherListView.height + launcherListView.topMargin + launcherListView.originY
399+ if (launcherListView.contentY < maxY) {
400+ launcherListView.contentY = Math.min(launcherListView.contentY + units.dp(2), maxY)
401+ }
402+ }
156403 }
157404 }
158405 }
406+ }
159407
160− MouseArea {
161− id: topFoldingArea
162− anchors {
163− left: parent.left
164− right: parent.right
165− top: parent.top
166− topMargin: launcherListView.topMargin
167− }
168− height: launcherListView.itemSize / 2
169− enabled: launcherListView.contentY > -launcherListView.topMargin
170− onClicked: launcherListView.flick(0, launcherListView.clickFlickSpeed)
408+ LauncherDelegate {
409+ id: fakeDragItem
410+ objectName: "fakeDragItem"
411+ visible: dndArea.draggedIndex >= 0 && !dndArea.postDragging
412+ itemWidth: launcherListView.itemWidth
413+ itemHeight: launcherListView.itemHeight
414+ height: itemHeight
415+ width: itemWidth
416+ rotation: root.rotation
417+ highlighted: true
418+ itemOpacity: 0.8
419+
420+ function flatten() {
421+ fakeDragItemAnimation.start();
171422 }
172423
173− MouseArea {
174− id: bottomFoldingArea
175− anchors {
176− left: parent.left
177− right: parent.right
178− bottom: parent.bottom
179− bottomMargin: launcherListView.bottomMargin
424+ UbuntuNumberAnimation {
425+ id: fakeDragItemAnimation
426+ target: fakeDragItem;
427+ properties: "angle,offset";
428+ to: 0
429+ }
430+ }
431+ }
432+ }
433+
434+ Component {
435+ id: popoverComponent
436+
437+ Popover {
438+ id: popover
439+ property var model
440+ property string appId
441+ contentWidth: quickListColumn.width
442+
443+ // FIXME: There's a bug in the Popover positioning that it covers the item in case it is rotated.
444+ // https://bugs.launchpad.net/ubuntu-ui-toolkit/+bug/1204470
445+ // For now, let's move the Popover around with callerMargin.
446+ // Remove popupMargin once the bug is fixed.
447+ property int popupMargin: root.inverted ? launcherListView.itemHeight : 0;
448+
449+ callerMargin: units.gu(1) + popupMargin
450+
451+ Column {
452+ id: quickListColumn
453+ width: units.gu(30)
454+ height: childrenRect.height
455+
456+ Repeater {
457+ model: popover.model
458+
459+ ListItems.Standard {
460+ objectName: "quickListEntry" + index
461+ text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
462+ highlightWhenPressed: model.clickable
463+ onClicked: {
464+ if (!model.clickable) {
465+ return;
466+ }
467+ LauncherModel.quickListActionInvoked(appId, index);
468+ PopupUtils.close(popover);
469+ }
180470 }
181− height: launcherListView.itemSize / 2
182− enabled: launcherListView.contentHeight - launcherListView.height - launcherListView.contentY > -launcherListView.bottomMargin
183− onClicked: launcherListView.flick(0, -launcherListView.clickFlickSpeed)
184471 }
185472 }
186473 }