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