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