Unity 8
 All Classes Functions
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  // We need the appId in the delegate in order to find
150  // the right app when running autopilot tests for
151  // multiple apps.
152  readonly property string appId: model.appId
153  itemHeight: launcherListView.itemHeight
154  itemWidth: launcherListView.itemWidth
155  width: itemWidth
156  height: itemHeight
157  iconName: model.icon
158  count: model.count
159  progress: model.progress
160  clipCorner: model.pinned
161  itemFocused: model.focused
162  inverted: root.inverted
163  z: -Math.abs(offset)
164  maxAngle: 55
165  property bool dragging: false
166 
167  ThinDivider {
168  id: dropIndicator
169  objectName: "dropIndicator"
170  anchors.centerIn: parent
171  width: parent.width + mainColumn.anchors.leftMargin + mainColumn.anchors.rightMargin
172  opacity: 0
173  source: "graphics/divider-line.png"
174  }
175 
176  states: [
177  State {
178  name: "selected"
179  when: dndArea.selectedItem === launcherDelegate && fakeDragItem.visible && !dragging
180  PropertyChanges {
181  target: launcherDelegate
182  itemOpacity: 0
183  }
184  },
185  State {
186  name: "dragging"
187  when: dragging
188  PropertyChanges {
189  target: launcherDelegate
190  height: units.gu(1)
191  itemOpacity: 0
192  }
193  PropertyChanges {
194  target: dropIndicator
195  opacity: 1
196  }
197  },
198  State {
199  name: "expanded"
200  when: dndArea.draggedIndex >= 0 && (dndArea.preDragging || dndArea.dragging || dndArea.postDragging) && dndArea.draggedIndex != index
201  PropertyChanges {
202  target: launcherDelegate
203  angle: 0
204  offset: 0
205  itemOpacity: 0.6
206  }
207  }
208  ]
209 
210  transitions: [
211  Transition {
212  from: ""
213  to: "selected"
214  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
215  },
216  Transition {
217  from: "*"
218  to: "expanded"
219  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
220  UbuntuNumberAnimation { properties: "angle,offset" }
221  },
222  Transition {
223  from: "expanded"
224  to: ""
225  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
226  UbuntuNumberAnimation { properties: "angle,offset" }
227  },
228  Transition {
229  id: draggingTransition
230  from: "selected"
231  to: "dragging"
232  SequentialAnimation {
233  PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: true }
234  ParallelAnimation {
235  UbuntuNumberAnimation { properties: "height" }
236  NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
237  }
238  ScriptAction {
239  script: {
240  if (launcherListView.scheduledMoveTo > -1) {
241  launcherListView.model.move(dndArea.draggedIndex, launcherListView.scheduledMoveTo)
242  dndArea.draggedIndex = launcherListView.scheduledMoveTo
243  launcherListView.scheduledMoveTo = -1
244  }
245  }
246  }
247  PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: false }
248  }
249  },
250  Transition {
251  from: "dragging"
252  to: "*"
253  NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.SnapDuration }
254  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
255  SequentialAnimation {
256  ScriptAction { script: if (index == launcherListView.count-1) launcherListView.flick(0, -launcherListView.clickFlickSpeed); }
257  UbuntuNumberAnimation { properties: "height" }
258  ScriptAction { script: if (index == launcherListView.count-1) launcherListView.flick(0, -launcherListView.clickFlickSpeed); }
259  PropertyAction { target: dndArea; property: "postDragging"; value: false }
260  PropertyAction { target: dndArea; property: "draggedIndex"; value: -1 }
261  }
262  }
263  ]
264  }
265 
266  MouseArea {
267  id: dndArea
268  objectName: "dndArea"
269  anchors {
270  fill: parent
271  topMargin: launcherListView.topMargin
272  bottomMargin: launcherListView.bottomMargin
273  }
274  drag.minimumY: -launcherListView.topMargin
275  drag.maximumY: height + launcherListView.bottomMargin
276 
277  property int draggedIndex: -1
278  property var selectedItem
279  property bool preDragging: false
280  property bool dragging: selectedItem !== undefined && selectedItem !== null && selectedItem.dragging
281  property bool postDragging: false
282  property int startX
283  property int startY
284 
285  onPressed: {
286  selectedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
287  }
288 
289  onClicked: {
290  var index = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
291  var clickedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
292 
293  // Check if we actually clicked an item or only at the spacing in between
294  if (clickedItem === null) {
295  return;
296  }
297 
298  // First/last item do the scrolling at more than 12 degrees
299  if (index == 0 || index == launcherListView.count - 1) {
300  if (clickedItem.angle > 12) {
301  launcherListView.flick(0, -launcherListView.clickFlickSpeed);
302  } else if (clickedItem.angle < -12) {
303  launcherListView.flick(0, launcherListView.clickFlickSpeed);
304  } else {
305  root.applicationSelected(LauncherModel.get(index).appId);
306  }
307  return;
308  }
309 
310  // the rest launches apps up to an angle of 30 degrees
311  if (clickedItem.angle > 30) {
312  launcherListView.flick(0, -launcherListView.clickFlickSpeed);
313  } else if (clickedItem.angle < -30) {
314  launcherListView.flick(0, launcherListView.clickFlickSpeed);
315  } else {
316  root.applicationSelected(LauncherModel.get(index).appId);
317  }
318  }
319 
320  onCanceled: {
321  selectedItem = undefined;
322  preDragging = false;
323  postDragging = false;
324  }
325 
326  onReleased: {
327  var droppedIndex = draggedIndex;
328  if (dragging) {
329  postDragging = true;
330  } else {
331  draggedIndex = -1;
332  }
333 
334  if (!selectedItem) {
335  return;
336  }
337 
338  selectedItem.dragging = false;
339  selectedItem = undefined;
340  preDragging = false;
341 
342  drag.target = undefined
343 
344  progressiveScrollingTimer.stop();
345  launcherListView.interactive = true;
346  if (droppedIndex >= launcherListView.count - 2 && postDragging) {
347  launcherListView.flick(0, -launcherListView.clickFlickSpeed);
348  }
349  if (droppedIndex == 0 && postDragging) {
350  launcherListView.flick(0, launcherListView.clickFlickSpeed);
351  }
352  }
353 
354  onPressAndHold: {
355  if (Math.abs(selectedItem.angle) > 30) {
356  return;
357  }
358 
359  draggedIndex = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
360 
361  // Opening QuickList
362  quickList.item = selectedItem;
363  quickList.model = launcherListView.model.get(draggedIndex).quickList;
364  quickList.appId = launcherListView.model.get(draggedIndex).appId;
365  quickList.state = "open";
366 
367  launcherListView.interactive = false
368 
369  var yOffset = draggedIndex > 0 ? (mouseY + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouseY + launcherListView.realContentY
370 
371  fakeDragItem.iconName = launcherListView.model.get(draggedIndex).icon
372  fakeDragItem.x = units.gu(0.5)
373  fakeDragItem.y = mouseY - yOffset + launcherListView.anchors.topMargin + launcherListView.topMargin
374  fakeDragItem.angle = selectedItem.angle * (root.inverted ? -1 : 1)
375  fakeDragItem.offset = selectedItem.offset * (root.inverted ? -1 : 1)
376  fakeDragItem.count = LauncherModel.get(draggedIndex).count
377  fakeDragItem.progress = LauncherModel.get(draggedIndex).progress
378  fakeDragItem.flatten()
379  drag.target = fakeDragItem
380 
381  startX = mouseX
382  startY = mouseY
383  }
384 
385  onPositionChanged: {
386  if (draggedIndex >= 0) {
387  if (!selectedItem.dragging) {
388  var distance = Math.max(Math.abs(mouseX - startX), Math.abs(mouseY - startY))
389  if (!preDragging && distance > units.gu(1.5)) {
390  preDragging = true;
391  quickList.state = "";
392  }
393  if (distance > launcherListView.itemHeight) {
394  selectedItem.dragging = true
395  preDragging = false;
396  }
397  }
398  if (!selectedItem.dragging) {
399  return
400  }
401 
402  var itemCenterY = fakeDragItem.y + fakeDragItem.height / 2
403 
404  // Move it down by the the missing size to compensate index calculation with only expanded items
405  itemCenterY += (launcherListView.itemHeight - selectedItem.height) / 2
406 
407  if (mouseY > launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin - launcherListView.realItemHeight) {
408  progressiveScrollingTimer.downwards = false
409  progressiveScrollingTimer.start()
410  } else if (mouseY < launcherListView.realItemHeight) {
411  progressiveScrollingTimer.downwards = true
412  progressiveScrollingTimer.start()
413  } else {
414  progressiveScrollingTimer.stop()
415  }
416 
417  var newIndex = (itemCenterY + launcherListView.realContentY) / launcherListView.realItemHeight
418 
419  if (newIndex > draggedIndex + 1) {
420  newIndex = draggedIndex + 1
421  } else if (newIndex < draggedIndex) {
422  newIndex = draggedIndex -1
423  } else {
424  return
425  }
426 
427  if (newIndex >= 0 && newIndex < launcherListView.count) {
428  if (launcherListView.draggingTransitionRunning) {
429  launcherListView.scheduledMoveTo = newIndex
430  } else {
431  launcherListView.model.move(draggedIndex, newIndex)
432  draggedIndex = newIndex
433  }
434  }
435  }
436  }
437  }
438  Timer {
439  id: progressiveScrollingTimer
440  interval: 2
441  repeat: true
442  running: false
443  property bool downwards: true
444  onTriggered: {
445  if (downwards) {
446  var minY = -launcherListView.topMargin
447  if (launcherListView.contentY > minY) {
448  launcherListView.contentY = Math.max(launcherListView.contentY - units.dp(2), minY)
449  }
450  } else {
451  var maxY = launcherListView.contentHeight - launcherListView.height + launcherListView.topMargin + launcherListView.originY
452  if (launcherListView.contentY < maxY) {
453  launcherListView.contentY = Math.min(launcherListView.contentY + units.dp(2), maxY)
454  }
455  }
456  }
457  }
458  }
459  }
460 
461  LauncherDelegate {
462  id: fakeDragItem
463  objectName: "fakeDragItem"
464  visible: dndArea.draggedIndex >= 0 && !dndArea.postDragging
465  itemWidth: launcherListView.itemWidth
466  itemHeight: launcherListView.itemHeight
467  height: itemHeight
468  width: itemWidth
469  rotation: root.rotation
470  itemOpacity: 0.9
471  clipCorner: dndArea.draggedIndex > -1 &&
472  LauncherModel.get(dndArea.draggedIndex).pinned &&
473  !dndArea.preDragging &&
474  !dndArea.dragging
475 
476  function flatten() {
477  fakeDragItemAnimation.start();
478  }
479 
480  UbuntuNumberAnimation {
481  id: fakeDragItemAnimation
482  target: fakeDragItem;
483  properties: "angle,offset";
484  to: 0
485  }
486  }
487  }
488  }
489 
490  UbuntuShapeForItem {
491  id: quickListShape
492  objectName: "quickListShape"
493  anchors.fill: quickList
494  opacity: quickList.state === "open" ? 0.8 : 0
495  visible: opacity > 0
496  rotation: root.rotation
497 
498  Behavior on opacity {
499  UbuntuNumberAnimation {}
500  }
501 
502  image: quickList
503 
504  Image {
505  anchors {
506  right: parent.left
507  rightMargin: -units.dp(4)
508  verticalCenter: parent.verticalCenter
509  verticalCenterOffset: -quickList.offset
510  }
511  height: units.gu(1)
512  width: units.gu(2)
513  source: "graphics/quicklist_tooltip.png"
514  rotation: 90
515  }
516 
517  InverseMouseArea {
518  anchors.fill: parent
519  enabled: quickList.state == "open"
520  onClicked: {
521  quickList.state = ""
522  }
523  }
524 
525  }
526 
527  Rectangle {
528  id: quickList
529  objectName: "quickList"
530  color: "#221e1c"
531  width: units.gu(30)
532  height: quickListColumn.height
533  visible: quickListShape.visible
534  anchors {
535  left: root.inverted ? undefined : parent.right
536  right: root.inverted ? parent.left : undefined
537  margins: units.gu(1)
538  }
539  y: itemCenter - (height / 2) + offset
540  rotation: root.rotation
541 
542  property var model
543  property string appId
544  property var item
545 
546  // internal
547  property int itemCenter: item ? root.mapFromItem(quickList.item).y + (item.height / 2) : units.gu(1)
548  property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
549  itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
550 
551  Column {
552  id: quickListColumn
553  width: parent.width
554  height: childrenRect.height
555 
556  Repeater {
557  id: popoverRepeater
558  model: quickList.model
559 
560  ListItems.Standard {
561  objectName: "quickListEntry" + index
562  text: (model.clickable ? "" : "<b>") + model.label + (model.clickable ? "" : "</b>")
563  highlightWhenPressed: model.clickable
564 
565  // FIXME: This is a workaround for the theme not being context sensitive. I.e. the
566  // ListItems don't know that they are sitting in a themed Popover where the color
567  // needs to be inverted.
568  __foregroundColor: Theme.palette.selected.backgroundText
569 
570  onClicked: {
571  if (!model.clickable) {
572  return;
573  }
574  quickList.state = "";
575  // Unsetting model to prevent showing changing entries during fading out
576  // that may happen because of triggering an action.
577  LauncherModel.quickListActionInvoked(quickList.appId, index);
578  quickList.model = undefined;
579  }
580  }
581  }
582  }
583  }
584 }