Unity 8
LauncherPanel.qml
1 /*
2  * Copyright (C) 2013-2016 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.4
18 import Ubuntu.Components 1.3
19 import Unity.Launcher 0.1
20 import Ubuntu.Components.Popups 1.3
21 import "../Components/ListItems"
22 import "../Components"
23 
24 Rectangle {
25  id: root
26  color: "#F2111111"
27 
28  rotation: inverted ? 180 : 0
29 
30  property var model
31  property bool inverted: false
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  || mouseEventEater.containsMouse || dashItem.hovered
36  property int highlightIndex: -2
37  property bool shortcutHintsShown: false
38 
39  signal applicationSelected(string appId)
40  signal showDashHome()
41  signal kbdNavigationCancelled()
42 
43  onXChanged: {
44  if (quickList.state == "open") {
45  quickList.state = ""
46  }
47  }
48 
49  function highlightNext() {
50  highlightIndex++;
51  if (highlightIndex >= launcherListView.count) {
52  highlightIndex = -1;
53  }
54  launcherListView.moveToIndex(Math.max(highlightIndex, 0));
55  }
56  function highlightPrevious() {
57  highlightIndex--;
58  if (highlightIndex <= -2) {
59  highlightIndex = launcherListView.count - 1;
60  }
61  launcherListView.moveToIndex(Math.max(highlightIndex, 0));
62  }
63  function openQuicklist(index) {
64  quickList.open(index);
65  quickList.selectedIndex = 0;
66  quickList.focus = true;
67  }
68 
69  MouseArea {
70  id: mouseEventEater
71  anchors.fill: parent
72  acceptedButtons: Qt.AllButtons
73  hoverEnabled: true
74  onWheel: wheel.accepted = true;
75  }
76 
77  Column {
78  id: mainColumn
79  anchors {
80  fill: parent
81  }
82 
83  Rectangle {
84  objectName: "buttonShowDashHome"
85  width: parent.width
86  height: width * .9
87  color: UbuntuColors.orange
88  readonly property bool highlighted: root.highlightIndex == -1;
89 
90  Image {
91  objectName: "dashItem"
92  width: parent.width * .6
93  height: width
94  anchors.centerIn: parent
95  source: "graphics/home.png"
96  rotation: root.rotation
97  }
98  AbstractButton {
99  id: dashItem
100  anchors.fill: parent
101  activeFocusOnPress: false
102  onClicked: root.showDashHome()
103  }
104  Rectangle {
105  objectName: "bfbFocusHighlight"
106  anchors.fill: parent
107  border.color: "white"
108  border.width: units.dp(1)
109  color: "transparent"
110  visible: parent.highlighted
111  }
112  }
113 
114  Item {
115  anchors.left: parent.left
116  anchors.right: parent.right
117  height: parent.height - dashItem.height - parent.spacing*2
118 
119  Item {
120  id: launcherListViewItem
121  anchors.fill: parent
122  clip: true
123 
124  ListView {
125  id: launcherListView
126  objectName: "launcherListView"
127  anchors {
128  fill: parent
129  topMargin: -extensionSize + width * .15
130  bottomMargin: -extensionSize + width * .15
131  }
132  topMargin: extensionSize
133  bottomMargin: extensionSize
134  height: parent.height - dashItem.height - parent.spacing*2
135  model: root.model
136  cacheBuffer: itemHeight * 3
137  snapMode: interactive ? ListView.SnapToItem : ListView.NoSnap
138  highlightRangeMode: ListView.ApplyRange
139  preferredHighlightBegin: (height - itemHeight) / 2
140  preferredHighlightEnd: (height + itemHeight) / 2
141 
142  // for the single peeking icon, when alert-state is set on delegate
143  property int peekingIndex: -1
144 
145  // The size of the area the ListView is extended to make sure items are not
146  // destroyed when dragging them outside the list. This needs to be at least
147  // itemHeight to prevent folded items from disappearing and DragArea limits
148  // need to be smaller than this size to avoid breakage.
149  property int extensionSize: 0
150 
151  // Setting extensionSize after the list has been populated because it has
152  // the potential to mess up with the intial positioning in combination
153  // with snapping to the center of the list. This catches all the cases
154  // where the item would be outside the list for more than itemHeight / 2.
155  // For the rest, give it a flick to scroll to the beginning. Note that
156  // the flicking alone isn't enough because in some cases it's not strong
157  // enough to overcome the snapping.
158  // https://bugreports.qt-project.org/browse/QTBUG-32251
159  Component.onCompleted: {
160  extensionSize = itemHeight * 3
161  flick(0, clickFlickSpeed)
162  }
163 
164  // The height of the area where icons start getting folded
165  property int foldingStartHeight: itemHeight
166  // The height of the area where the items reach the final folding angle
167  property int foldingStopHeight: foldingStartHeight - itemHeight - spacing
168  property int itemWidth: width * .75
169  property int itemHeight: itemWidth * 15 / 16 + units.gu(1)
170  property int clickFlickSpeed: units.gu(60)
171  property int draggedIndex: dndArea.draggedIndex
172  property real realContentY: contentY - originY + topMargin
173  property int realItemHeight: itemHeight + spacing
174 
175  // In case the start dragging transition is running, we need to delay the
176  // move because the displaced transition would clash with it and cause items
177  // to be moved to wrong places
178  property bool draggingTransitionRunning: false
179  property int scheduledMoveTo: -1
180 
181  UbuntuNumberAnimation {
182  id: snapToBottomAnimation
183  target: launcherListView
184  property: "contentY"
185  to: launcherListView.originY + launcherListView.topMargin
186  }
187 
188  UbuntuNumberAnimation {
189  id: snapToTopAnimation
190  target: launcherListView
191  property: "contentY"
192  to: launcherListView.contentHeight - launcherListView.height + launcherListView.originY - launcherListView.topMargin
193  }
194 
195  UbuntuNumberAnimation {
196  id: moveAnimation
197  objectName: "moveAnimation"
198  target: launcherListView
199  property: "contentY"
200  function moveTo(contentY) {
201  from = launcherListView.contentY;
202  to = contentY;
203  restart();
204  }
205  }
206  function moveToIndex(index) {
207  var totalItemHeight = launcherListView.itemHeight + launcherListView.spacing
208  var itemPosition = index * totalItemHeight;
209  var height = launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin
210  var distanceToEnd = index == 0 || index == launcherListView.count - 1 ? 0 : totalItemHeight
211  if (itemPosition + totalItemHeight + distanceToEnd > launcherListView.contentY + launcherListView.originY + launcherListView.topMargin + height) {
212  moveAnimation.moveTo(itemPosition + launcherListView.itemHeight - launcherListView.topMargin - height + distanceToEnd - launcherListView.originY);
213  } else if (itemPosition - distanceToEnd < launcherListView.contentY - launcherListView.originY + launcherListView.topMargin) {
214  moveAnimation.moveTo(itemPosition - distanceToEnd - launcherListView.topMargin + launcherListView.originY);
215  }
216  }
217 
218  displaced: Transition {
219  NumberAnimation { properties: "x,y"; duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing }
220  }
221 
222  delegate: FoldingLauncherDelegate {
223  id: launcherDelegate
224  objectName: "launcherDelegate" + index
225  // We need the appId in the delegate in order to find
226  // the right app when running autopilot tests for
227  // multiple apps.
228  readonly property string appId: model.appId
229  itemIndex: index
230  itemHeight: launcherListView.itemHeight
231  itemWidth: launcherListView.itemWidth
232  width: parent.width
233  height: itemHeight
234  iconName: model.icon
235  count: model.count
236  countVisible: model.countVisible
237  progress: model.progress
238  itemRunning: model.running
239  itemFocused: model.focused
240  inverted: root.inverted
241  alerting: model.alerting
242  highlighted: root.highlightIndex == index
243  shortcutHintShown: root.shortcutHintsShown && index <= 9
244  surfaceCount: model.surfaceCount
245  z: -Math.abs(offset)
246  maxAngle: 55
247  property bool dragging: false
248 
249  SequentialAnimation {
250  id: peekingAnimation
251 
252  // revealing
253  PropertyAction { target: root; property: "visible"; value: (launcher.visibleWidth === 0) ? 1 : 0 }
254  PropertyAction { target: launcherListViewItem; property: "clip"; value: 0 }
255 
256  UbuntuNumberAnimation {
257  target: launcherDelegate
258  alwaysRunToEnd: true
259  loops: 1
260  properties: "x"
261  to: (units.gu(.5) + launcherListView.width * .5) * (root.inverted ? -1 : 1)
262  duration: UbuntuAnimation.BriskDuration
263  }
264 
265  // hiding
266  UbuntuNumberAnimation {
267  target: launcherDelegate
268  alwaysRunToEnd: true
269  loops: 1
270  properties: "x"
271  to: 0
272  duration: UbuntuAnimation.BriskDuration
273  }
274 
275  PropertyAction { target: launcherListViewItem; property: "clip"; value: 1 }
276  PropertyAction { target: root; property: "visible"; value: (launcher.visibleWidth === 0) ? 0 : 1 }
277  PropertyAction { target: launcherListView; property: "peekingIndex"; value: -1 }
278  }
279 
280  onAlertingChanged: {
281  if(alerting) {
282  if (!dragging && (launcherListView.peekingIndex === -1 || launcher.visibleWidth > 0)) {
283  launcherListView.moveToIndex(index)
284  if (!dragging && launcher.state !== "visible") {
285  peekingAnimation.start()
286  }
287  }
288 
289  if (launcherListView.peekingIndex === -1) {
290  launcherListView.peekingIndex = index
291  }
292  } else {
293  if (launcherListView.peekingIndex === index) {
294  launcherListView.peekingIndex = -1
295  }
296  }
297  }
298 
299  ThinDivider {
300  id: dropIndicator
301  objectName: "dropIndicator"
302  anchors.centerIn: parent
303  width: parent.width + mainColumn.anchors.leftMargin + mainColumn.anchors.rightMargin
304  opacity: 0
305  source: "graphics/divider-line.png"
306  }
307 
308  states: [
309  State {
310  name: "selected"
311  when: dndArea.selectedItem === launcherDelegate && fakeDragItem.visible && !dragging
312  PropertyChanges {
313  target: launcherDelegate
314  itemOpacity: 0
315  }
316  },
317  State {
318  name: "dragging"
319  when: dragging
320  PropertyChanges {
321  target: launcherDelegate
322  height: units.gu(1)
323  itemOpacity: 0
324  }
325  PropertyChanges {
326  target: dropIndicator
327  opacity: 1
328  }
329  },
330  State {
331  name: "expanded"
332  when: dndArea.draggedIndex >= 0 && (dndArea.preDragging || dndArea.dragging || dndArea.postDragging) && dndArea.draggedIndex != index
333  PropertyChanges {
334  target: launcherDelegate
335  angle: 0
336  offset: 0
337  itemOpacity: 0.6
338  }
339  }
340  ]
341 
342  transitions: [
343  Transition {
344  from: ""
345  to: "selected"
346  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
347  },
348  Transition {
349  from: "*"
350  to: "expanded"
351  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.FastDuration }
352  UbuntuNumberAnimation { properties: "angle,offset" }
353  },
354  Transition {
355  from: "expanded"
356  to: ""
357  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
358  UbuntuNumberAnimation { properties: "angle,offset" }
359  },
360  Transition {
361  id: draggingTransition
362  from: "selected"
363  to: "dragging"
364  SequentialAnimation {
365  PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: true }
366  ParallelAnimation {
367  UbuntuNumberAnimation { properties: "height" }
368  NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.FastDuration }
369  }
370  ScriptAction {
371  script: {
372  if (launcherListView.scheduledMoveTo > -1) {
373  launcherListView.model.move(dndArea.draggedIndex, launcherListView.scheduledMoveTo)
374  dndArea.draggedIndex = launcherListView.scheduledMoveTo
375  launcherListView.scheduledMoveTo = -1
376  }
377  }
378  }
379  PropertyAction { target: launcherListView; property: "draggingTransitionRunning"; value: false }
380  }
381  },
382  Transition {
383  from: "dragging"
384  to: "*"
385  NumberAnimation { target: dropIndicator; properties: "opacity"; duration: UbuntuAnimation.SnapDuration }
386  NumberAnimation { properties: "itemOpacity"; duration: UbuntuAnimation.BriskDuration }
387  SequentialAnimation {
388  ScriptAction { script: if (index == launcherListView.count-1) launcherListView.flick(0, -launcherListView.clickFlickSpeed); }
389  UbuntuNumberAnimation { properties: "height" }
390  ScriptAction { script: if (index == launcherListView.count-1) launcherListView.flick(0, -launcherListView.clickFlickSpeed); }
391  PropertyAction { target: dndArea; property: "postDragging"; value: false }
392  PropertyAction { target: dndArea; property: "draggedIndex"; value: -1 }
393  }
394  }
395  ]
396  }
397 
398  MouseArea {
399  id: dndArea
400  objectName: "dndArea"
401  acceptedButtons: Qt.LeftButton | Qt.RightButton
402  anchors {
403  fill: parent
404  topMargin: launcherListView.topMargin
405  bottomMargin: launcherListView.bottomMargin
406  }
407  drag.minimumY: -launcherListView.topMargin
408  drag.maximumY: height + launcherListView.bottomMargin
409 
410  property int draggedIndex: -1
411  property var selectedItem
412  property bool preDragging: false
413  property bool dragging: !!selectedItem && selectedItem.dragging
414  property bool postDragging: false
415  property int startX
416  property int startY
417 
418  onPressed: {
419  processPress(mouse);
420  }
421 
422  function processPress(mouse) {
423  selectedItem = launcherListView.itemAt(mouse.x, mouse.y + launcherListView.realContentY)
424  }
425 
426  onClicked: {
427  var index = Math.floor((mouseY + launcherListView.realContentY) / launcherListView.realItemHeight);
428  var clickedItem = launcherListView.itemAt(mouseX, mouseY + launcherListView.realContentY)
429 
430  // Check if we actually clicked an item or only at the spacing in between
431  if (clickedItem === null) {
432  return;
433  }
434 
435  if (mouse.button & Qt.RightButton) { // context menu
436  // Opening QuickList
437  quickList.open(index);
438  return;
439  }
440 
441  Haptics.play();
442 
443  // First/last item do the scrolling at more than 12 degrees
444  if (index == 0 || index == launcherListView.count - 1) {
445  if (clickedItem.angle > 12 || clickedItem.angle < -12) {
446  launcherListView.moveToIndex(index);
447  } else {
448  root.applicationSelected(LauncherModel.get(index).appId);
449  }
450  return;
451  }
452 
453  // the rest launches apps up to an angle of 30 degrees
454  if (clickedItem.angle > 30 || clickedItem.angle < -30) {
455  launcherListView.moveToIndex(index);
456  } else {
457  root.applicationSelected(LauncherModel.get(index).appId);
458  }
459  }
460 
461  onCanceled: {
462  endDrag(drag);
463  }
464 
465  onReleased: {
466  endDrag(drag);
467  }
468 
469  function endDrag(dragItem) {
470  var droppedIndex = draggedIndex;
471  if (dragging) {
472  postDragging = true;
473  } else {
474  draggedIndex = -1;
475  }
476 
477  if (!selectedItem) {
478  return;
479  }
480 
481  selectedItem.dragging = false;
482  selectedItem = undefined;
483  preDragging = false;
484 
485  dragItem.target = undefined
486 
487  progressiveScrollingTimer.stop();
488  launcherListView.interactive = true;
489  if (droppedIndex >= launcherListView.count - 2 && postDragging) {
490  snapToBottomAnimation.start();
491  } else if (droppedIndex < 2 && postDragging) {
492  snapToTopAnimation.start();
493  }
494  }
495 
496  onPressAndHold: {
497  processPressAndHold(mouse, drag);
498  }
499 
500  function processPressAndHold(mouse, dragItem) {
501  if (Math.abs(selectedItem.angle) > 30) {
502  return;
503  }
504 
505  Haptics.play();
506 
507  draggedIndex = Math.floor((mouse.y + launcherListView.realContentY) / launcherListView.realItemHeight);
508 
509  quickList.open(draggedIndex)
510 
511  launcherListView.interactive = false
512 
513  var yOffset = draggedIndex > 0 ? (mouse.y + launcherListView.realContentY) % (draggedIndex * launcherListView.realItemHeight) : mouse.y + launcherListView.realContentY
514 
515  fakeDragItem.iconName = launcherListView.model.get(draggedIndex).icon
516  fakeDragItem.x = units.gu(0.5)
517  fakeDragItem.y = mouse.y - yOffset + launcherListView.anchors.topMargin + launcherListView.topMargin
518  fakeDragItem.angle = selectedItem.angle * (root.inverted ? -1 : 1)
519  fakeDragItem.offset = selectedItem.offset * (root.inverted ? -1 : 1)
520  fakeDragItem.count = LauncherModel.get(draggedIndex).count
521  fakeDragItem.progress = LauncherModel.get(draggedIndex).progress
522  fakeDragItem.flatten()
523  dragItem.target = fakeDragItem
524 
525  startX = mouse.x
526  startY = mouse.y
527  }
528 
529  onPositionChanged: {
530  processPositionChanged(mouse)
531  }
532 
533  function processPositionChanged(mouse) {
534  if (draggedIndex >= 0) {
535  if (!selectedItem.dragging) {
536  var distance = Math.max(Math.abs(mouse.x - startX), Math.abs(mouse.y - startY))
537  if (!preDragging && distance > units.gu(1.5)) {
538  preDragging = true;
539  quickList.state = "";
540  }
541  if (distance > launcherListView.itemHeight) {
542  selectedItem.dragging = true
543  preDragging = false;
544  }
545  }
546  if (!selectedItem.dragging) {
547  return
548  }
549 
550  var itemCenterY = fakeDragItem.y + fakeDragItem.height / 2
551 
552  // Move it down by the the missing size to compensate index calculation with only expanded items
553  itemCenterY += (launcherListView.itemHeight - selectedItem.height) / 2
554 
555  if (mouseY > launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin - launcherListView.realItemHeight) {
556  progressiveScrollingTimer.downwards = false
557  progressiveScrollingTimer.start()
558  } else if (mouseY < launcherListView.realItemHeight) {
559  progressiveScrollingTimer.downwards = true
560  progressiveScrollingTimer.start()
561  } else {
562  progressiveScrollingTimer.stop()
563  }
564 
565  var newIndex = (itemCenterY + launcherListView.realContentY) / launcherListView.realItemHeight
566 
567  if (newIndex > draggedIndex + 1) {
568  newIndex = draggedIndex + 1
569  } else if (newIndex < draggedIndex) {
570  newIndex = draggedIndex -1
571  } else {
572  return
573  }
574 
575  if (newIndex >= 0 && newIndex < launcherListView.count) {
576  if (launcherListView.draggingTransitionRunning) {
577  launcherListView.scheduledMoveTo = newIndex
578  } else {
579  launcherListView.model.move(draggedIndex, newIndex)
580  draggedIndex = newIndex
581  }
582  }
583  }
584  }
585  }
586  Timer {
587  id: progressiveScrollingTimer
588  interval: 2
589  repeat: true
590  running: false
591  property bool downwards: true
592  onTriggered: {
593  if (downwards) {
594  var minY = -launcherListView.topMargin
595  if (launcherListView.contentY > minY) {
596  launcherListView.contentY = Math.max(launcherListView.contentY - units.dp(2), minY)
597  }
598  } else {
599  var maxY = launcherListView.contentHeight - launcherListView.height + launcherListView.topMargin + launcherListView.originY
600  if (launcherListView.contentY < maxY) {
601  launcherListView.contentY = Math.min(launcherListView.contentY + units.dp(2), maxY)
602  }
603  }
604  }
605  }
606  }
607  }
608 
609  LauncherDelegate {
610  id: fakeDragItem
611  objectName: "fakeDragItem"
612  visible: dndArea.draggedIndex >= 0 && !dndArea.postDragging
613  itemWidth: launcherListView.itemWidth
614  itemHeight: launcherListView.itemHeight
615  height: itemHeight
616  width: itemWidth
617  rotation: root.rotation
618  itemOpacity: 0.9
619  onVisibleChanged: if (!visible) iconName = "";
620 
621  function flatten() {
622  fakeDragItemAnimation.start();
623  }
624 
625  UbuntuNumberAnimation {
626  id: fakeDragItemAnimation
627  target: fakeDragItem;
628  properties: "angle,offset";
629  to: 0
630  }
631  }
632  }
633  }
634 
635  UbuntuShapeForItem {
636  id: quickListShape
637  objectName: "quickListShape"
638  anchors.fill: quickList
639  opacity: quickList.state === "open" ? 0.95 : 0
640  visible: opacity > 0
641  rotation: root.rotation
642  aspect: UbuntuShape.Flat
643 
644  Behavior on opacity {
645  UbuntuNumberAnimation {}
646  }
647 
648  image: quickList
649 
650  Image {
651  anchors {
652  right: parent.left
653  rightMargin: -units.dp(4)
654  verticalCenter: parent.verticalCenter
655  verticalCenterOffset: -quickList.offset * (root.inverted ? -1 : 1)
656  }
657  height: units.gu(1)
658  width: units.gu(2)
659  source: "graphics/quicklist_tooltip.png"
660  rotation: 90
661  }
662  }
663 
664  InverseMouseArea {
665  anchors.fill: quickListShape
666  enabled: quickList.state == "open" || pressed
667 
668  onClicked: {
669  quickList.state = "";
670  quickList.focus = false;
671  root.kbdNavigationCancelled();
672  }
673 
674  // Forward for dragging to work when quickList is open
675 
676  onPressed: {
677  var m = mapToItem(dndArea, mouseX, mouseY)
678  dndArea.processPress(m)
679  }
680 
681  onPressAndHold: {
682  var m = mapToItem(dndArea, mouseX, mouseY)
683  dndArea.processPressAndHold(m, drag)
684  }
685 
686  onPositionChanged: {
687  var m = mapToItem(dndArea, mouseX, mouseY)
688  dndArea.processPositionChanged(m)
689  }
690 
691  onCanceled: {
692  dndArea.endDrag(drag);
693  }
694 
695  onReleased: {
696  dndArea.endDrag(drag);
697  }
698  }
699 
700  Rectangle {
701  id: quickList
702  objectName: "quickList"
703  color: theme.palette.normal.background
704  // Because we're setting left/right anchors depending on orientation, it will break the
705  // width setting after rotating twice. This makes sure we also re-apply width on rotation
706  width: root.inverted ? units.gu(30) : units.gu(30)
707  height: quickListColumn.height
708  visible: quickListShape.visible
709  anchors {
710  left: root.inverted ? undefined : parent.right
711  right: root.inverted ? parent.left : undefined
712  margins: units.gu(1)
713  }
714  y: itemCenter - (height / 2) + offset
715  rotation: root.rotation
716 
717  property var model
718  property string appId
719  property var item
720  property int selectedIndex: -1
721 
722  Keys.onPressed: {
723  switch (event.key) {
724  case Qt.Key_Down:
725  selectedIndex++;
726  if (selectedIndex >= popoverRepeater.count) {
727  selectedIndex = 0;
728  }
729  event.accepted = true;
730  break;
731  case Qt.Key_Up:
732  selectedIndex--;
733  if (selectedIndex < 0) {
734  selectedIndex = popoverRepeater.count - 1;
735  }
736  event.accepted = true;
737  break;
738  case Qt.Key_Left:
739  case Qt.Key_Escape:
740  quickList.selectedIndex = -1;
741  quickList.focus = false;
742  quickList.state = ""
743  event.accepted = true;
744  break;
745  case Qt.Key_Enter:
746  case Qt.Key_Return:
747  case Qt.Key_Space:
748  if (quickList.selectedIndex >= 0) {
749  LauncherModel.quickListActionInvoked(quickList.appId, quickList.selectedIndex)
750  }
751  quickList.selectedIndex = -1;
752  quickList.focus = false;
753  quickList.state = ""
754  root.kbdNavigationCancelled();
755  event.accepted = true;
756  break;
757  }
758  }
759 
760  // internal
761  property int itemCenter: item ? root.mapFromItem(quickList.item, 0, 0).y + (item.height / 2) + quickList.item.offset : units.gu(1)
762  property int offset: itemCenter + (height/2) + units.gu(1) > parent.height ? -itemCenter - (height/2) - units.gu(1) + parent.height :
763  itemCenter - (height/2) < units.gu(1) ? (height/2) - itemCenter + units.gu(1) : 0
764 
765  function open(index) {
766  var itemPosition = index * launcherListView.itemHeight;
767  var height = launcherListView.height - launcherListView.topMargin - launcherListView.bottomMargin
768  item = launcherListView.itemAt(launcherListView.width / 2, itemPosition + launcherListView.itemHeight / 2);
769  quickList.model = launcherListView.model.get(index).quickList;
770  quickList.appId = launcherListView.model.get(index).appId;
771  quickList.state = "open";
772  }
773 
774  Item {
775  width: parent.width
776  height: quickListColumn.height
777 
778  Column {
779  id: quickListColumn
780  width: parent.width
781  height: childrenRect.height
782 
783  Repeater {
784  id: popoverRepeater
785  model: quickList.model
786 
787  ListItem {
788  objectName: "quickListEntry" + index
789  selected: index === quickList.selectedIndex
790  height: label.implicitHeight + label.anchors.topMargin + label.anchors.bottomMargin
791  color: model.clickable ? (selected ? theme.palette.highlighted.background : "transparent") : theme.palette.disabled.background
792  highlightColor: !model.clickable ? quickList.color : undefined // make disabled items visually unclickable
793  divider.colorFrom: UbuntuColors.inkstone
794  divider.colorTo: UbuntuColors.inkstone
795 
796  Label {
797  id: label
798  anchors.fill: parent
799  anchors.leftMargin: units.gu(3) // 2 GU for checkmark, 3 GU total
800  anchors.rightMargin: units.gu(2)
801  anchors.topMargin: units.gu(2)
802  anchors.bottomMargin: units.gu(2)
803  verticalAlignment: Label.AlignVCenter
804  text: model.label
805  fontSize: index == 0 ? "medium" : "small"
806  font.weight: index == 0 ? Font.Medium : Font.Light
807  color: model.clickable ? theme.palette.normal.backgroundText : theme.palette.disabled.backgroundText
808  }
809 
810  onClicked: {
811  if (!model.clickable) {
812  return;
813  }
814  Haptics.play();
815  quickList.state = "";
816  // Unsetting model to prevent showing changing entries during fading out
817  // that may happen because of triggering an action.
818  LauncherModel.quickListActionInvoked(quickList.appId, index);
819  quickList.focus = false;
820  root.kbdNavigationCancelled();
821  quickList.model = undefined;
822  }
823  }
824  }
825  }
826  }
827  }
828 }