@@ -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,429 @@ 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+ // In case the start dragging transition is running, we need to delay the
125+ // move because the displaced transition would clash with it and cause items
126+ // to be moved to wrong places
127+ property bool draggingTransitionRunning: false
128+ property int scheduledMoveTo: -1
129+
130+ displaced: Transition {
131+ NumberAnimation { properties: "x,y"; duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing }
132+ }
133+
134+ delegate: FoldingLauncherDelegate {
135+ id: launcherDelegate
136+ objectName: "launcherDelegate" + index
137+ itemHeight: launcherListView.itemHeight
138+ itemWidth: launcherListView.itemWidth
139+ width: itemWidth
140+ height: itemHeight
141+ iconName: model.icon
142+ count: model.count
143+ progress: model.progress
144+ inverted: root.inverted
145+ highlighted: dragging && index === root.highlightIndex
146+ z: -Math.abs(offset)
147+ maxAngle: 60
148+ property bool dragging: false
149+
150+ ThinDivider {
151+ id: dropIndicator
152+ objectName: "dropIndicator"
153+ anchors.centerIn: parent
154+ width: parent.width + mainColumn.anchors.leftMargin + mainColumn.anchors.rightMargin
155+ opacity: 0
156+ }
157+
158+ states: [
159+ State {
160+ name: "selected"
161+ when: dndArea.selectedItem === launcherDelegate && fakeDragItem.visible && !dragging
162+ PropertyChanges {
163+ target: launcherDelegate
164+ itemOpacity: 0
165+ }
166+ },
167+ State {
168+ name: "dragging"
169+ when: dragging
170+ PropertyChanges {
171+ target: launcherDelegate
172+ height: units.gu(1)
173+ itemOpacity: 0
174+ }
175+ PropertyChanges {
176+ target: dropIndicator
177+ opacity: 1
178+ }
179+ },
180+ State {
181+ name: "expanded"
182+ when: dndArea.draggedIndex >= 0 && (dndArea.preDragging || dndArea.dragging || dndArea.postDragging) && dndArea.draggedIndex != index
183+ PropertyChanges {
184+ target: launcherDelegate
185+ angle: 0
186+ offset: 0
187+ itemOpacity: 0.6
188+ }
189+ }
190+ ]
191+
192+ transitions: [
193+ Transition {
194+ from: ""
195+ to: "selected"
196+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
197+ },
198+ Transition {
199+ from: "*"
200+ to: "expanded"
201+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
202+ UbuntuNumberAnimation { properties: "angle,offset" }
203+ },
204+ Transition {
205+ from: "expanded"
206+ to: ""
207+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
208+ UbuntuNumberAnimation { properties: "angle,offset" }
209+ },
210+ Transition {
211+ id: draggingTransition
212+ from: "selected"
213+ to: "dragging"
214+ SequentialAnimation {
215+ PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: true }
216+ ParallelAnimation {
217+ UbuntuNumberAnimation { properties: "height" }
218+ NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
219+ }
220+ ScriptAction {
221+ script: {
222+ if (launcherListView.scheduledMoveTo > -1) {
223+ launcherListView.model.move(dndArea.draggedIndex, launcherListView.scheduledMoveTo)
224+ dndArea.draggedIndex = launcherListView.scheduledMoveTo
225+ launcherListView.scheduledMoveTo = -1
226+ }
227+ }
228+ }
229+ PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: false }
230+ }
231+ },
232+ Transition {
233+ from: "dragging"
234+ to: "*"
235+ NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
236+ NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
237+ SequentialAnimation {
238+ UbuntuNumberAnimation { properties: "height" }
239+ PropertyAction { target: dndArea; property: "postDragging"; value: false }
240+ PropertyAction { target: dndArea; property: "draggedIndex"; value: -1 }
241+ }
242+ }
243+ ]
244+ }
245+
246+ MouseArea {
247+ id: dndArea
248+ objectName: "dndArea"
249+ anchors {
250+ fill: parent
251+ topMargin: launcherListView.topMargin
252+ bottomMargin: launcherListView.bottomMargin
253+ }
254+ drag.minimumY: -launcherListView.topMargin
255+ drag.maximumY: height + launcherListView.bottomMargin
256+
257+ property int draggedIndex: -1
258+ property var selectedItem
259+ property bool preDragging: false
260+ property bool dragging: selectedItem !== undefined && selectedItem !== null && selectedItem.dragging
261+ property bool postDragging: false
262+ property int startX
263+ property int startY
264+ property var quickListPopover: null
265+
266+ onPressed: {
267+ selectedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
268+ if (selectedItem !== null) {
269+ selectedItem.highlighted = true
270+ }
271+ }
272+
273+ onClicked: {
274+ var index = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
275+ var clickedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
276+
277+ // Check if we actually clicked an item or only at the spacing in between
278+ if (clickedItem === null) {
279+ return;
280+ }
281+
282+ // First/last item do the scrolling at more than 12 degrees
283+ if (index == 0 || index == launcherListView.count - 1) {
284+ if (clickedItem.angle > 12) {
285+ launcherListView.flick(0, -launcherListView.clickFlickSpeed);
286+ } else if (clickedItem.angle < -12) {
287+ launcherListView.flick(0, launcherListView.clickFlickSpeed);
288+ } else {
289+ root.applicationSelected(LauncherModel.get(index).desktopFile);
290+ }
291+ return;
292+ }
293+
294+ // the rest launches apps up to an angle of 30 degrees
295+ if (clickedItem.angle > 30) {
140296 launcherListView.flick(0, -launcherListView.clickFlickSpeed);
141− } else if (angle < -12) {
297+ } else if (clickedItem.angle < -30) {
142298 launcherListView.flick(0, launcherListView.clickFlickSpeed);
143299 } else {
144300 root.applicationSelected(LauncherModel.get(index).desktopFile);
145301 }
146− return;
147302 }
148303
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);
304+ onCanceled: {
305+ selectedItem.highlighted = false
306+ selectedItem = undefined;
307+ preDragging = false;
308+ postDragging = false;
309+ }
310+
311+ onReleased: {
312+ var droppedIndex = draggedIndex;
313+ if (dragging) {
314+ postDragging = true;
315+ } else {
316+ draggedIndex = -1;
317+ }
318+
319+ if (!selectedItem) {
320+ return;
321+ }
322+
323+ selectedItem.highlighted = false
324+ selectedItem.dragging = false;
325+ selectedItem = undefined;
326+ preDragging = false;
327+
328+ drag.target = undefined
329+
330+ progressiveScrollingTimer.stop();
331+ launcherListView.interactive = true;
332+ if (droppedIndex >= launcherListView.count - 2 && postDragging) {
333+ launcherListView.flick(0, -launcherListView.clickFlickSpeed);
334+ }
335+ }
336+
337+ onPressAndHold: {
338+ if (Math.abs(selectedItem.angle) > 30) {
339+ return;
340+ }
341+
342+ draggedIndex = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
343+
344+ // Opening QuickList
345+ var quickListModel = launcherListView.model.get(draggedIndex).quickList
346+ var quickListAppId = launcherListView.model.get(draggedIndex).appId
347+
348+ quickListPopover = PopupUtils.open(popoverComponent, selectedItem,
349+ {model: quickListModel, appId: quickListAppId})
350+
351+ launcherListView.interactive = false
352+
353+ var yOffset = draggedIndex > 0 ? (mouseY + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouseY + launcherListView.realContentY
354+
355+ fakeDragItem.iconName = launcherListView.model.get(draggedIndex).icon
356+ fakeDragItem.x = 0
357+ fakeDragItem.y = mouseY - yOffset + launcherListView.anchors.topMargin + launcherListView.topMargin
358+ fakeDragItem.angle = selectedItem.angle * (root.inverted ? -1 : 1)
359+ fakeDragItem.offset = selectedItem.offset * (root.inverted ? -1 : 1)
360+ fakeDragItem.count = LauncherModel.get(draggedIndex).count
361+ fakeDragItem.progress = LauncherModel.get(draggedIndex).progress
362+ fakeDragItem.flatten()
363+ drag.target = fakeDragItem
364+
365+ startX = mouseX
366+ startY = mouseY
367+ }
368+
369+ onPositionChanged: {
370+ if (draggedIndex >= 0) {
371+ if (!selectedItem.dragging) {
372+ var distance = Math.max(Math.abs(mouseX - startX), Math.abs(mouseY - startY))
373+ if (!preDragging && distance > units.gu(1.5)) {
374+ preDragging = true;
375+ PopupUtils.close(quickListPopover)
376+ }
377+ if (distance > launcherListView.itemHeight) {
378+ selectedItem.dragging = true
379+ preDragging = false;
380+ }
381+ }
382+ if (!selectedItem.dragging) {
383+ return
384+ }
385+
386+ var itemCenterY = fakeDragItem.y + fakeDragItem.height / 2
387+
388+ // Move it down by the the missing size to compensate index calculation with only expanded items
389+ itemCenterY += (launcherListView.itemHeight - selectedItem.height) / 2
390+
391+ if (mouseY > launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin - launcherListView.realItemHeight) {
392+ progressiveScrollingTimer.downwards = false
393+ progressiveScrollingTimer.start()
394+ } else if (mouseY < launcherListView.realItemHeight) {
395+ progressiveScrollingTimer.downwards = true
396+ progressiveScrollingTimer.start()
397+ } else {
398+ progressiveScrollingTimer.stop()
399+ }
400+
401+ var newIndex = (itemCenterY + launcherListView.realContentY) / launcherListView.realItemHeight
402+
403+ if (newIndex > draggedIndex + 1) {
404+ newIndex = draggedIndex + 1
405+ } else if (newIndex < draggedIndex) {
406+ newIndex = draggedIndex -1
407+ } else {
408+ return
409+ }
410+
411+ if (newIndex >= 0 && newIndex < launcherListView.count) {
412+ if (launcherListView.draggingTransitionRunning) {
413+ launcherListView.scheduledMoveTo = newIndex
414+ } else {
415+ launcherListView.model.move(draggedIndex, newIndex)
416+ draggedIndex = newIndex
417+ }
418+ }
419+ }
420+ }
421+ }
422+ Timer {
423+ id: progressiveScrollingTimer
424+ interval: 2
425+ repeat: true
426+ running: false
427+ property bool downwards: true
428+ onTriggered: {
429+ if (downwards) {
430+ var minY = -launcherListView.topMargin
431+ if (launcherListView.contentY > minY) {
432+ launcherListView.contentY = Math.max(launcherListView.contentY - units.dp(2), minY)
433+ }
434+ } else {
435+ var maxY = launcherListView.contentHeight - launcherListView.height + launcherListView.topMargin + launcherListView.originY
436+ if (launcherListView.contentY < maxY) {
437+ launcherListView.contentY = Math.min(launcherListView.contentY + units.dp(2), maxY)
438+ }
439+ }
156440 }
157441 }
158442 }
443+ }
159444
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)
445+ LauncherDelegate {
446+ id: fakeDragItem
447+ objectName: "fakeDragItem"
448+ visible: dndArea.draggedIndex >= 0 && !dndArea.postDragging
449+ itemWidth: launcherListView.itemWidth
450+ itemHeight: launcherListView.itemHeight
451+ height: itemHeight
452+ width: itemWidth
453+ rotation: root.rotation
454+ highlighted: true
455+ itemOpacity: 0.8
456+
457+ function flatten() {
458+ fakeDragItemAnimation.start();
171459 }
172460
173− MouseArea {
174− id: bottomFoldingArea
175− anchors {
176− left: parent.left
177− right: parent.right
178− bottom: parent.bottom
179− bottomMargin: launcherListView.bottomMargin
461+ UbuntuNumberAnimation {
462+ id: fakeDragItemAnimation
463+ target: fakeDragItem;
464+ properties: "angle,offset";
465+ to: 0
466+ }
467+ }
468+ }
469+ }
470+
471+ Component {
472+ id: popoverComponent
473+
474+ Popover {
475+ id: popover
476+ property var model
477+ property string appId
478+ contentWidth: quickListColumn.width
479+
480+ // FIXME: There's a bug in the Popover positioning that it covers the item in case it is rotated.
481+ // https://bugs.launchpad.net/ubuntu-ui-toolkit/+bug/1204470
482+ // For now, let's move the Popover around with callerMargin.
483+ // Remove popupMargin once the bug is fixed.
484+ property int popupMargin: root.inverted ? launcherListView.itemHeight : 0;
485+
486+ callerMargin: units.gu(1) + popupMargin
487+
488+ Column {
489+ id: quickListColumn
490+ width: units.gu(30)
491+ height: childrenRect.height
492+
493+ Repeater {
494+ model: popover.model
495+
496+ ListItems.Standard {
497+ objectName: "quickListEntry" + index
498+ text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
499+ highlightWhenPressed: model.clickable
500+ onClicked: {
501+ if (!model.clickable) {
502+ return;
503+ }
504+ LauncherModel.quickListActionInvoked(appId, index);
505+ PopupUtils.close(popover);
506+ }
180507 }
181− height: launcherListView.itemSize / 2
182− enabled: launcherListView.contentHeight - launcherListView.height - launcherListView.contentY > -launcherListView.bottomMargin
183− onClicked: launcherListView.flick(0, -launcherListView.clickFlickSpeed)
184508 }
185509 }
186510 }