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