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