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