Unity 8
DesktopSpread.qml
1 /*
2  * Copyright (C) 2015 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 QtQuick.Layouts 1.1
19 import Ubuntu.Components 1.3
20 import Ubuntu.Gestures 0.1
21 import Unity.Application 0.1
22 
23 FocusScope {
24  id: root
25 
26  property bool altTabPressed: false
27  property Item workspace: null
28 
29  readonly property alias highlightedIndex: spreadRepeater.highlightedIndex
30 
31  function show() {
32  spreadContainer.animateIn = true;
33  root.state = "altTab";
34  }
35 
36  onFocusChanged: {
37  // When the spread comes active, we want to keep focus to the input handler below
38  // Make sure nothing inside the ApplicationWindow grabs our focus!
39  if (focus) {
40  forceActiveFocus();
41  }
42  }
43 
44  Keys.onPressed: {
45  switch (event.key) {
46  case Qt.Key_Left:
47  case Qt.Key_Backtab:
48  selectPrevious(event.isAutoRepeat)
49  event.accepted = true;
50  break;
51  case Qt.Key_Right:
52  case Qt.Key_Tab:
53  selectNext(event.isAutoRepeat)
54  event.accepted = true;
55  break;
56  case Qt.Key_Escape:
57  spreadRepeater.highlightedIndex = -1
58  // Falling through intentionally
59  case Qt.Key_Enter:
60  case Qt.Key_Return:
61  case Qt.Key_Space:
62  root.state = ""
63  event.accepted = true;
64  }
65  }
66 
67  function selectNext(isAutoRepeat) {
68  if (isAutoRepeat && spreadRepeater.highlightedIndex >= ApplicationManager.count -1) {
69  return; // AutoRepeat is not allowed to wrap around
70  }
71 
72  spreadRepeater.highlightedIndex = (spreadRepeater.highlightedIndex + 1) % ApplicationManager.count;
73  var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, spreadRepeater.highlightedIndex - 3));
74  if (spreadFlickable.contentX < newContentX || spreadRepeater.highlightedIndex == 0) {
75  spreadFlickable.snapTo(newContentX)
76  }
77  }
78 
79  function selectPrevious(isAutoRepeat) {
80  if (isAutoRepeat && spreadRepeater.highlightedIndex == 0) {
81  return; // AutoRepeat is not allowed to wrap around
82  }
83 
84  var newIndex = spreadRepeater.highlightedIndex - 1 >= 0 ? spreadRepeater.highlightedIndex - 1 : ApplicationManager.count - 1;
85  spreadRepeater.highlightedIndex = newIndex;
86  var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, spreadRepeater.highlightedIndex - 1));
87  if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
88  spreadFlickable.snapTo(newContentX)
89  }
90  }
91 
92  function focusSelected() {
93  if (spreadRepeater.highlightedIndex != -1) {
94  var application = ApplicationManager.get(spreadRepeater.highlightedIndex);
95  ApplicationManager.requestFocusApplication(application.appId);
96  }
97  }
98 
99  function cancel() {
100  spreadRepeater.highlightedIndex = -1;
101  state = ""
102  }
103 
104  Item {
105  id: spreadContainer
106  objectName: "spreadContainer"
107  anchors.fill: parent
108  visible: false
109 
110  property bool animateIn: false
111 
112  Repeater {
113  id: spreadRepeater
114  objectName: "spreadRepeater"
115  model: ApplicationManager
116 
117  property int highlightedIndex: -1
118  property int closingIndex: -1
119 
120  function indexOf(delegateItem) {
121  for (var i = 0; i < spreadRepeater.count; i++) {
122  if (spreadRepeater.itemAt(i) === delegateItem) {
123  return i;
124  }
125  }
126  return -1;
127  }
128 
129  delegate: Item {
130  id: spreadDelegate
131  objectName: "spreadDelegate"
132  width: units.gu(20)
133  height: units.gu(20)
134 
135  property real angle: 0
136  property real itemScale: 1
137  property int itemScaleOriginX: 0
138  property int itemScaleOriginY: 0
139 
140  Behavior on x {
141  id: closeBehavior
142  enabled: spreadRepeater.closingIndex >= 0
143  UbuntuNumberAnimation {
144  onRunningChanged: if (!running) spreadRepeater.closingIndex = -1
145  }
146  }
147 
148  DesktopSpreadDelegate {
149  id: clippedSpreadDelegate
150  objectName: "clippedSpreadDelegate"
151  anchors.left: parent.left
152  anchors.top: parent.top
153  application: ApplicationManager.get(index)
154  width: spreadMaths.spreadHeight
155  height: spreadMaths.spreadHeight
156 
157  transform: [
158  Scale {
159  origin.x: itemScaleOriginX
160  origin.y: itemScaleOriginY
161  xScale: itemScale
162  yScale: itemScale
163  },
164  Rotation {
165  origin { x: 0; y: (clippedSpreadDelegate.height - (clippedSpreadDelegate.height * itemScale / 2)) }
166  axis { x: 0; y: 1; z: 0 }
167  angle: spreadDelegate.angle
168  }
169  ]
170 
171  MouseArea {
172  id: spreadSelectArea
173  anchors.fill: parent
174  anchors.margins: -units.gu(2)
175  enabled: false
176  onClicked: {
177  spreadRepeater.highlightedIndex = index;
178  root.state = "";
179  }
180  }
181  }
182 
183  SpreadMaths {
184  id: spreadMaths
185  flickable: spreadFlickable
186  itemIndex: index
187  totalItems: Math.max(6, ApplicationManager.count)
188  sceneHeight: root.height
189  itemHeight: spreadDelegate.height
190  }
191 
192  states: [
193  State {
194  name: "altTab"; when: root.state == "altTab" && spreadContainer.visible
195  PropertyChanges {
196  target: spreadDelegate
197  x: spreadMaths.animatedX
198  y: spreadMaths.animatedY + (spreadDelegate.height - clippedSpreadDelegate.height) - units.gu(2)
199  width: spreadMaths.spreadHeight
200  height: spreadMaths.sceneHeight
201  angle: spreadMaths.animatedAngle
202  itemScale: spreadMaths.scale
203  itemScaleOriginY: clippedSpreadDelegate.height / 2;
204  z: index
205  visible: spreadMaths.itemVisible
206  }
207  PropertyChanges {
208  target: clippedSpreadDelegate
209  highlightShown: index == spreadRepeater.highlightedIndex
210  state: "transformed"
211  shadowOpacity: spreadMaths.shadowOpacity
212  anchors.topMargin: units.gu(2)
213  }
214  PropertyChanges {
215  target: tileInfo
216  visible: true
217  opacity: spreadMaths.tileInfoOpacity
218  }
219  PropertyChanges {
220  target: spreadSelectArea
221  enabled: true
222  }
223  }
224  ]
225  transitions: [
226  Transition {
227  from: ""
228  to: "altTab"
229  PropertyAction { target: spreadDelegate; properties: "y,height,width,angle,z,itemScale,itemScaleOriginY,visible" }
230  PropertyAction { target: clippedSpreadDelegate; properties: "anchors.topMargin" }
231  PropertyAnimation {
232  target: spreadDelegate; properties: "x"
233  from: root.width
234  duration: spreadContainer.animateIn ? UbuntuAnimation.FastDuration :0
235  easing: UbuntuAnimation.StandardEasing
236  }
237  }
238  ]
239 
240  MouseArea {
241  id: tileInfo
242  objectName: "tileInfo"
243  anchors { left: parent.left; top: clippedSpreadDelegate.bottom; topMargin: units.gu(5) }
244  property int nextItemX: spreadRepeater.count > index + 1 ? spreadRepeater.itemAt(index + 1).x : spreadDelegate.x + units.gu(30)
245  width: Math.min(units.gu(30), nextItemX - spreadDelegate.x)
246  height: titleInfoColumn.height
247  visible: false
248  hoverEnabled: true
249 
250  onContainsMouseChanged: {
251  if (containsMouse) {
252  spreadRepeater.highlightedIndex = index
253  }
254  }
255 
256  onClicked: {
257  root.state = ""
258  }
259 
260  ColumnLayout {
261  id: titleInfoColumn
262  anchors { left: parent.left; top: parent.top; right: parent.right }
263  spacing: units.gu(1)
264 
265  UbuntuShape {
266  Layout.preferredHeight: Math.min(units.gu(6), root.height * .05)
267  Layout.preferredWidth: height * 8 / 7.6
268  image: Image {
269  anchors.fill: parent
270  source: model.icon
271  }
272  }
273  Label {
274  Layout.fillWidth: true
275  Layout.preferredHeight: units.gu(6)
276  text: model.name
277  wrapMode: Text.WordWrap
278  elide: Text.ElideRight
279  maximumLineCount: 2
280  }
281  }
282  }
283 
284  Image {
285  id: closeImage
286  anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset + units.gu(2) }
287  source: "graphics/window-close.svg"
288  readonly property var mousePos: hoverMouseArea.mapToItem(spreadDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
289  visible: index == spreadRepeater.highlightedIndex
290  && mousePos.y < (clippedSpreadDelegate.height / 3)
291  && mousePos.y > -units.gu(4)
292  && mousePos.x > -units.gu(4)
293  && mousePos.x < (clippedSpreadDelegate.width * 2 / 3)
294  height: units.gu(1.5)
295  width: height
296  sourceSize.width: width
297  sourceSize.height: height
298 
299  MouseArea {
300  id: closeMouseArea
301  objectName: "closeMouseArea"
302  anchors.fill: closeImage
303  anchors.margins: -units.gu(2)
304  onClicked: {
305  spreadRepeater.closingIndex = index;
306  ApplicationManager.stopApplication(model.appId)
307  }
308  }
309  }
310  }
311  }
312  }
313 
314 
315  MouseArea {
316  id: hoverMouseArea
317  objectName: "hoverMouseArea"
318  anchors.fill: spreadContainer
319  propagateComposedEvents: true
320  hoverEnabled: true
321  enabled: false
322  visible: enabled
323 
324  property int scrollAreaWidth: root.width / 3
325  property bool progressiveScrollingEnabled: false
326 
327  onMouseXChanged: {
328  mouse.accepted = false
329 
330  if (hoverMouseArea.pressed) {
331  return;
332  }
333 
334  // Find the hovered item and mark it active
335  var mapped = mapToItem(spreadContainer, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
336  var itemUnder = spreadContainer.childAt(mapped.x, mapped.y)
337  if (itemUnder) {
338  mapped = mapToItem(itemUnder, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
339  var delegateChild = itemUnder.childAt(mapped.x, mapped.y)
340  if (delegateChild && (delegateChild.objectName === "clippedSpreadDelegate" || delegateChild.objectName === "tileInfo")) {
341  spreadRepeater.highlightedIndex = spreadRepeater.indexOf(itemUnder)
342  }
343  }
344 
345  if (spreadFlickable.contentWidth > spreadFlickable.minContentWidth) {
346  var margins = spreadFlickable.width * 0.05;
347 
348  if (!progressiveScrollingEnabled && mouseX < spreadFlickable.width - scrollAreaWidth) {
349  progressiveScrollingEnabled = true
350  }
351 
352  // do we need to scroll?
353  if (mouseX < scrollAreaWidth + margins) {
354  var progress = Math.min(1, (scrollAreaWidth + margins - mouseX) / (scrollAreaWidth - margins));
355  var contentX = (1 - progress) * (spreadFlickable.contentWidth - spreadFlickable.width)
356  spreadFlickable.contentX = Math.max(0, Math.min(spreadFlickable.contentX, contentX))
357  }
358  if (mouseX > spreadFlickable.width - scrollAreaWidth && progressiveScrollingEnabled) {
359  var progress = Math.min(1, (mouseX - (spreadFlickable.width - scrollAreaWidth)) / (scrollAreaWidth - margins))
360  var contentX = progress * (spreadFlickable.contentWidth - spreadFlickable.width)
361  spreadFlickable.contentX = Math.min(spreadFlickable.contentWidth - spreadFlickable.width, Math.max(spreadFlickable.contentX, contentX))
362  }
363  }
364  }
365  onPressed: mouse.accepted = false
366  }
367 
368  FloatingFlickable {
369  id: spreadFlickable
370  objectName: "spreadFlickable"
371  anchors.fill: parent
372  property int minContentWidth: 6 * Math.min(height / 4, width / 5)
373  contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
374  enabled: false
375 
376  function snapTo(contentX) {
377  snapAnimation.stop();
378  snapAnimation.to = contentX
379  snapAnimation.start();
380  }
381 
382  UbuntuNumberAnimation {
383  id: snapAnimation
384  target: spreadFlickable
385  property: "contentX"
386  }
387  }
388 
389  Item {
390  id: workspaceSelector
391  anchors {
392  left: parent.left
393  top: parent.top
394  right: parent.right
395  topMargin: units.gu(3.5)
396  }
397  height: root.height * 0.25
398  visible: false
399 
400  RowLayout {
401  anchors.fill: parent
402  spacing: units.gu(1)
403  Item { Layout.fillWidth: true }
404  Repeater {
405  model: 1 // TODO: will be a workspacemodel in the future
406  Item {
407  Layout.fillHeight: true
408  Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
409  Image {
410  source: root.background
411  anchors {
412  left: parent.left
413  right: parent.right
414  verticalCenter: parent.verticalCenter
415  }
416  height: parent.height * 0.75
417 
418  ShaderEffect {
419  anchors.fill: parent
420 
421  property var source: ShaderEffectSource {
422  id: shaderEffectSource
423  sourceItem: appContainer
424  }
425 
426  fragmentShader: "
427  varying highp vec2 qt_TexCoord0;
428  uniform sampler2D source;
429  void main(void)
430  {
431  highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
432  gl_FragColor = sourceColor;
433  }"
434  }
435  }
436 
437  // TODO: This is the bar for the currently selected workspace
438  // Enable this once the workspace stuff is implemented
439  // Rectangle {
440  // anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
441  // height: units.dp(2)
442  // color: UbuntuColors.orange
443  // visible: index == 0 // TODO: should be active workspace index
444  // }
445  }
446 
447  }
448  // TODO: This is the "new workspace" button. Enable this once workspaces are implemented
449  // Item {
450  // Layout.fillHeight: true
451  // Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
452  // Rectangle {
453  // anchors {
454  // left: parent.left
455  // right: parent.right
456  // verticalCenter: parent.verticalCenter
457  // }
458  // height: parent.height * 0.75
459  // color: "#22ffffff"
460 
461  // Label {
462  // anchors.centerIn: parent
463  // font.pixelSize: parent.height / 2
464  // text: "+"
465  // }
466  // }
467  // }
468  Item { Layout.fillWidth: true }
469  }
470  }
471 
472  Label {
473  id: currentSelectedLabel
474  anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
475  text: spreadRepeater.highlightedIndex >= 0 ? ApplicationManager.get(spreadRepeater.highlightedIndex).name : ""
476  visible: false
477  fontSize: "large"
478  }
479 
480  states: [
481  State {
482  name: "altTab"; when: root.altTabPressed
483  PropertyChanges { target: blurLayer; saturation: 0.8; blurRadius: 60; visible: true }
484  PropertyChanges { target: workspaceSelector; visible: true }
485  PropertyChanges { target: spreadContainer; visible: true }
486  PropertyChanges { target: spreadFlickable; enabled: spreadFlickable.contentWidth > spreadFlickable.minContentWidth }
487  PropertyChanges { target: currentSelectedLabel; visible: true }
488  PropertyChanges { target: spreadBackground; visible: true }
489  PropertyChanges { target: hoverMouseArea; enabled: true }
490  }
491  ]
492  transitions: [
493  Transition {
494  from: "*"
495  to: "altTab"
496  SequentialAnimation {
497  PropertyAction { target: hoverMouseArea; property: "progressiveScrollingEnabled"; value: false }
498  PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
499  PauseAnimation { duration: 140 }
500  PropertyAction { target: workspaceSelector; property: "visible" }
501  PropertyAction { target: spreadContainer; property: "visible" }
502  ParallelAnimation {
503  UbuntuNumberAnimation {
504  target: blurLayer; properties: "saturation,blurRadius";
505  duration: spreadContainer.animateIn ? UbuntuAnimation.FastDuration : 0
506  }
507  PropertyAction { target: spreadFlickable; property: "visible" }
508  PropertyAction { targets: [currentSelectedLabel,spreadBackground]; property: "visible" }
509  PropertyAction { target: spreadFlickable; property: "contentX"; value: 0 }
510  }
511  }
512  },
513  Transition {
514  from: "*"
515  to: "*"
516  PropertyAnimation { property: "opacity" }
517  ScriptAction { script: { root.focusSelected() } }
518  PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: -1 }
519  PropertyAction { target: spreadContainer; property: "animateIn"; value: false }
520  }
521 
522  ]
523 }