Unity 8
 All Classes Functions Properties
LauncherPanel.qml
1 /*
2  * Copyright (C) 2013 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.0
18 import Ubuntu.Components 0.1
19 import Ubuntu.Components.ListItems 0.1 as ListItems
20 import Unity.Launcher 0.1
21 import Ubuntu.Components.Popups 0.1
22 import "../Components/ListItems"
23 import "../Components/"
24 
25 Item {
26  id: root
27 
28  rotation: inverted ? 180 : 0
29 
30  property var model
31  property bool inverted: true
32  property bool dragging: false
33  property bool moving: launcherListView.moving || launcherListView.flicking
34  property bool preventHiding: moving || dndArea.draggedIndex >= 0 || quickList.state === "open" || dndArea.pressed
35  property int highlightIndex: -1
36 
37  signal applicationSelected(string appId)
38  signal showDashHome()
39 
40  BorderImage {
41  id: background
42  source: "graphics/launcher_bg.sci"
43  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
47  }
48 
49  Column {
50  id: mainColumn
51  anchors {
52  fill: parent
53  }
54 
55  MouseArea {
56  id: dashItem
57  width: parent.width
58  height: units.gu(7)
59  onClicked: root.showDashHome()
60  z: 1
61  Image {
62  objectName: "dashItem"
63  width: units.gu(5)
64  height: width
65  anchors.centerIn: parent
66  source: "graphics/home.png"
67  rotation: root.rotation
68  }
69  }
70  ThinDivider {
71  anchors {
72  left: parent.left
73  right: parent.right
74  margins: -mainColumn.anchors.leftMargin
75  }
76  rotation: root.rotation
77  }
78 
79  Item {
80  anchors.left: parent.left
81  anchors.right: parent.right
82  height: parent.height - dashItem.height - parent.spacing*2
83 
84  Item {
85  anchors.fill: parent
86  clip: true
87 
88  ListView {
89  id: launcherListView
90  objectName: "launcherListView"
91  anchors {
92  fill: parent
93  topMargin: -extensionSize + units.gu(0.5)
94  bottomMargin: -extensionSize + units.gu(1)
95  leftMargin: units.gu(0.5)
96  rightMargin: units.gu(0.5)
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: 0
114 
115  // Setting extensionSize after the list has been populated because it has
116  // the potential to mess up with the intial positioning in combination
117  // with snapping to the center of the list. This catches all the cases
118  // where the item would be outside the list for more than itemHeight / 2.
119  // For the rest, give it a flick to scroll to the beginning. Note that
120  // the flicking alone isn't enough because in some cases it's not strong
121  // enough to overcome the snapping.
122  // https://bugreports.qt-project.org/browse/QTBUG-32251
123  Component.onCompleted: {
124  extensionSize = itemHeight * 3
125  flick(0, clickFlickSpeed)
126  }
127 
128  // The height of the area where icons start getting folded
129  property int foldingStartHeight: units.gu(6.5)
130  // The height of the area where the items reach the final folding angle
131  property int foldingStopHeight: foldingStartHeight - itemHeight - spacing
132  property int itemWidth: units.gu(7)
133  property int itemHeight: units.gu(6.5)
134  property int clickFlickSpeed: units.gu(60)
135  property int draggedIndex: dndArea.draggedIndex
136  property real realContentY: contentY - originY + topMargin
137  property int realItemHeight: itemHeight + spacing
138 
139  // In case the start dragging transition is running, we need to delay the
140  // move because the displaced transition would clash with it and cause items
141  // to be moved to wrong places
142  property bool draggingTransitionRunning: false
143  property int scheduledMoveTo: -1
144 
145  displaced: Transition {
146  NumberAnimation { properties: "x,y"; duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing }
147  }
148 
149  delegate: FoldingLauncherDelegate {
150  id: launcherDelegate
151  objectName: "launcherDelegate" + index
152  itemHeight: launcherListView.itemHeight
153  itemWidth: launcherListView.itemWidth
154  width: itemWidth
155  height: itemHeight
156  iconName: model.icon
157  count: model.count
158  progress: model.progress
159  itemFocused: model.focused
160  inverted: root.inverted
161  z: -Math.abs(offset)
162  maxAngle: 55
163  property bool dragging: false
164 
165  ThinDivider {
166  id: dropIndicator
167  objectName: "dropIndicator"
168  anchors.centerIn: parent
169  width: parent.width + mainColumn.anchors.leftMargin + mainColumn.anchors.rightMargin
170  opacity: 0
171  }
172 
173  states: [
174  State {
175  name: "selected"
176  when: dndArea.selectedItem === launcherDelegate && fakeDragItem.visible && !dragging
177  PropertyChanges {
178  target: launcherDelegate
179  itemOpacity: 0
180  }
181  },
182  State {
183  name: "dragging"
184  when: dragging
185  PropertyChanges {
186  target: launcherDelegate
187  height: units.gu(1)
188  itemOpacity: 0
189  }
190  PropertyChanges {
191  target: dropIndicator
192  opacity: 1
193  }
194  },
195  State {
196  name: "expanded"
197  when: dndArea.draggedIndex >= 0 && (dndArea.preDragging || dndArea.dragging || dndArea.postDragging) && dndArea.draggedIndex != index
198  PropertyChanges {
199  target: launcherDelegate
200  angle: 0
201  offset: 0
202  itemOpacity: 0.6
203  }
204  }
205  ]
206 
207  transitions: [
208  Transition {
209  from: ""
210  to: "selected"
211  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
212  },
213  Transition {
214  from: "*"
215  to: "expanded"
216  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
217  UbuntuNumberAnimation { properties: "angle,offset" }
218  },
219  Transition {
220  from: "expanded"
221  to: ""
222  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
223  UbuntuNumberAnimation { properties: "angle,offset" }
224  },
225  Transition {
226  id: draggingTransition
227  from: "selected"
228  to: "dragging"
229  SequentialAnimation {
230  PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: true }
231  ParallelAnimation {
232  UbuntuNumberAnimation { properties: "height" }
233  NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
234  }
235  ScriptAction {
236  script: {
237  if (launcherListView.scheduledMoveTo > -1) {
238  launcherListView.model.move(dndArea.draggedIndex, launcherListView.scheduledMoveTo)
239  dndArea.draggedIndex = launcherListView.scheduledMoveTo
240  launcherListView.scheduledMoveTo = -1
241  }
242  }
243  }
244  PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: false }
245  }
246  },
247  Transition {
248  from: "dragging"
249  to: "*"
250  NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
251  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
252  SequentialAnimation {
253  ScriptAction { script: if (index == launcherListView.count-1) launcherListView.flick(0, -launcherListView.clickFlickSpeed); }
254  UbuntuNumberAnimation { properties: "height" }
255  ScriptAction { script: if (index == launcherListView.count-1) launcherListView.flick(0, -launcherListView.clickFlickSpeed); }
256  PropertyAction { target: dndArea; property: "postDragging"; value: false }
257  PropertyAction { target: dndArea; property: "draggedIndex"; value: -1 }
258  }
259  }
260  ]
261  }
262 
263  MouseArea {
264  id: dndArea
265  objectName: "dndArea"
266  anchors {
267  fill: parent
268  topMargin: launcherListView.topMargin
269  bottomMargin: launcherListView.bottomMargin
270  }
271  drag.minimumY: -launcherListView.topMargin
272  drag.maximumY: height + launcherListView.bottomMargin
273 
274  property int draggedIndex: -1
275  property var selectedItem
276  property bool preDragging: false
277  property bool dragging: selectedItem !== undefined && selectedItem !== null && selectedItem.dragging
278  property bool postDragging: false
279  property int startX
280  property int startY
281 
282  onPressed: {
283  selectedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
284  }
285 
286  onClicked: {
287  var index = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
288  var clickedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
289 
290  // Check if we actually clicked an item or only at the spacing in between
291  if (clickedItem === null) {
292  return;
293  }
294 
295  // First/last item do the scrolling at more than 12 degrees
296  if (index == 0 || index == launcherListView.count - 1) {
297  if (clickedItem.angle > 12) {
298  launcherListView.flick(0, -launcherListView.clickFlickSpeed);
299  } else if (clickedItem.angle < -12) {
300  launcherListView.flick(0, launcherListView.clickFlickSpeed);
301  } else {
302  root.applicationSelected(LauncherModel.get(index).appId);
303  }
304  return;
305  }
306 
307  // the rest launches apps up to an angle of 30 degrees
308  if (clickedItem.angle > 30) {
309  launcherListView.flick(0, -launcherListView.clickFlickSpeed);
310  } else if (clickedItem.angle < -30) {
311  launcherListView.flick(0, launcherListView.clickFlickSpeed);
312  } else {
313  root.applicationSelected(LauncherModel.get(index).appId);
314  }
315  }
316 
317  onCanceled: {
318  selectedItem = undefined;
319  preDragging = false;
320  postDragging = false;
321  }
322 
323  onReleased: {
324  var droppedIndex = draggedIndex;
325  if (dragging) {
326  postDragging = true;
327  } else {
328  draggedIndex = -1;
329  }
330 
331  if (!selectedItem) {
332  return;
333  }
334 
335  selectedItem.dragging = false;
336  selectedItem = undefined;
337  preDragging = false;
338 
339  drag.target = undefined
340 
341  progressiveScrollingTimer.stop();
342  launcherListView.interactive = true;
343  if (droppedIndex >= launcherListView.count - 2 && postDragging) {
344  launcherListView.flick(0, -launcherListView.clickFlickSpeed);
345  }
346  if (droppedIndex == 0 && postDragging) {
347  launcherListView.flick(0, launcherListView.clickFlickSpeed);
348  }
349  }
350 
351  onPressAndHold: {
352  if (Math.abs(selectedItem.angle) > 30) {
353  return;
354  }
355 
356  draggedIndex = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
357 
358  // Opening QuickList
359  quickList.item = selectedItem;
360  quickList.model = launcherListView.model.get(draggedIndex).quickList;
361  quickList.appId = launcherListView.model.get(draggedIndex).appId;
362  quickList.state = "open";
363 
364  launcherListView.interactive = false
365 
366  var yOffset = draggedIndex > 0 ? (mouseY + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouseY + launcherListView.realContentY
367 
368  fakeDragItem.iconName = launcherListView.model.get(draggedIndex).icon
369  fakeDragItem.x = units.gu(0.5)
370  fakeDragItem.y = mouseY - yOffset + launcherListView.anchors.topMargin + launcherListView.topMargin
371  fakeDragItem.angle = selectedItem.angle * (root.inverted ? -1 : 1)
372  fakeDragItem.offset = selectedItem.offset * (root.inverted ? -1 : 1)
373  fakeDragItem.count = LauncherModel.get(draggedIndex).count
374  fakeDragItem.progress = LauncherModel.get(draggedIndex).progress
375  fakeDragItem.flatten()
376  drag.target = fakeDragItem
377 
378  startX = mouseX
379  startY = mouseY
380  }
381 
382  onPositionChanged: {
383  if (draggedIndex >= 0) {
384  if (!selectedItem.dragging) {
385  var distance = Math.max(Math.abs(mouseX - startX), Math.abs(mouseY - startY))
386  if (!preDragging && distance > units.gu(1.5)) {
387  preDragging = true;
388  quickList.state = "";
389  }
390  if (distance > launcherListView.itemHeight) {
391  selectedItem.dragging = true
392  preDragging = false;
393  }
394  }
395  if (!selectedItem.dragging) {
396  return
397  }
398 
399  var itemCenterY = fakeDragItem.y + fakeDragItem.height / 2
400 
401  // Move it down by the the missing size to compensate index calculation with only expanded items
402  itemCenterY += (launcherListView.itemHeight - selectedItem.height) / 2
403 
404  if (mouseY > launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin - launcherListView.realItemHeight) {
405  progressiveScrollingTimer.downwards = false
406  progressiveScrollingTimer.start()
407  } else if (mouseY < launcherListView.realItemHeight) {
408  progressiveScrollingTimer.downwards = true
409  progressiveScrollingTimer.start()
410  } else {
411  progressiveScrollingTimer.stop()
412  }
413 
414  var newIndex = (itemCenterY + launcherListView.realContentY) / launcherListView.realItemHeight
415 
416  if (newIndex > draggedIndex + 1) {
417  newIndex = draggedIndex + 1
418  } else if (newIndex < draggedIndex) {
419  newIndex = draggedIndex -1
420  } else {
421  return
422  }
423 
424  if (newIndex >= 0 && newIndex < launcherListView.count) {
425  if (launcherListView.draggingTransitionRunning) {
426  launcherListView.scheduledMoveTo = newIndex
427  } else {
428  launcherListView.model.move(draggedIndex, newIndex)
429  draggedIndex = newIndex
430  }
431  }
432  }
433  }
434  }
435  Timer {
436  id: progressiveScrollingTimer
437  interval: 2
438  repeat: true
439  running: false
440  property bool downwards: true
441  onTriggered: {
442  if (downwards) {
443  var minY = -launcherListView.topMargin
444  if (launcherListView.contentY > minY) {
445  launcherListView.contentY = Math.max(launcherListView.contentY - units.dp(2), minY)
446  }
447  } else {
448  var maxY = launcherListView.contentHeight - launcherListView.height + launcherListView.topMargin + launcherListView.originY
449  if (launcherListView.contentY < maxY) {
450  launcherListView.contentY = Math.min(launcherListView.contentY + units.dp(2), maxY)
451  }
452  }
453  }
454  }
455  }
456  }
457 
458  LauncherDelegate {
459  id: fakeDragItem
460  objectName: "fakeDragItem"
461  visible: dndArea.draggedIndex >= 0 && !dndArea.postDragging
462  itemWidth: launcherListView.itemWidth
463  itemHeight: launcherListView.itemHeight
464  height: itemHeight
465  width: itemWidth
466  rotation: root.rotation
467  itemOpacity: 0.9
468 
469  function flatten() {
470  fakeDragItemAnimation.start();
471  }
472 
473  UbuntuNumberAnimation {
474  id: fakeDragItemAnimation
475  target: fakeDragItem;
476  properties: "angle,offset";
477  to: 0
478  }
479  }
480  }
481  }
482 
483  UbuntuShapeForItem {
484  id: quickListShape
485  objectName: "quickListShape"
486  anchors.fill: quickList
487  opacity: quickList.state === "open" ? 0.8 : 0
488  visible: opacity > 0
489  rotation: root.rotation
490 
491  Behavior on opacity {
492  UbuntuNumberAnimation {}
493  }
494 
495  image: quickList
496 
497  Image {
498  anchors {
499  right: parent.left
500  rightMargin: -units.dp(4)
501  verticalCenter: parent.verticalCenter
502  verticalCenterOffset: -quickList.offset
503  }
504  height: units.gu(1)
505  width: units.gu(2)
506  source: "graphics/quicklist_tooltip.png"
507  rotation: 90
508  }
509 
510  InverseMouseArea {
511  anchors.fill: parent
512  enabled: quickList.state == "open"
513  onClicked: {
514  quickList.state = ""
515  }
516  }
517 
518  }
519 
520  Rectangle {
521  id: quickList
522  objectName: "quickList"
523  color: "#221e1c"
524  width: units.gu(30)
525  height: quickListColumn.height
526  anchors {
527  left: root.inverted ? undefined : parent.right
528  right: root.inverted ? parent.left : undefined
529  margins: units.gu(1)
530 
531  }
532  y: itemCenter - (height / 2) + offset
533  rotation: root.rotation
534 
535  property var model
536  property string appId
537  property var item
538 
539  // internal
540  property int itemCenter: item ? root.mapFromItem(quickList.item).y + (item.height / 2) : units.gu(1)
541  property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
542  itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
543 
544  Column {
545  id: quickListColumn
546  width: parent.width
547  height: childrenRect.height
548 
549  Repeater {
550  id: popoverRepeater
551  model: quickList.model
552 
553  ListItems.Standard {
554  objectName: "quickListEntry" + index
555  text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
556  highlightWhenPressed: model.clickable
557 
558  // FIXME: This is a workaround for the theme not being context sensitive. I.e. the
559  // ListItems don't know that they are sitting in a themed Popover where the color
560  // needs to be inverted.
561  __foregroundColor: Theme.palette.selected.backgroundText
562 
563  onClicked: {
564  if (!model.clickable) {
565  return;
566  }
567  quickList.state = "";
568  // Unsetting model to prevent showing changing entries during fading out
569  // that may happen because of triggering an action.
570  LauncherModel.quickListActionInvoked(quickList.appId, index);
571  quickList.model = undefined;
572  }
573  }
574  }
575  }
576  }
577 }