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