Unity 8
DesktopStage.qml
1 /*
2  * Copyright (C) 2014-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  * Authors: Michael Zanetti <michael.zanetti@canonical.com>
17  */
18 
19 import QtQuick 2.4
20 import QtQuick.Layouts 1.1
21 import Ubuntu.Components 1.1
22 import Unity.Application 0.1
23 import "../Components/PanelState"
24 import Utils 0.1
25 import Ubuntu.Gestures 0.1
26 
27 Rectangle {
28  id: root
29  anchors.fill: parent
30 
31  // Controls to be set from outside
32  property int dragAreaWidth // just to comply with the interface shared between stages
33  property real maximizedAppTopMargin
34  property bool interactive
35  property bool spreadEnabled // just to comply with the interface shared between stages
36  property real inverseProgress: 0 // just to comply with the interface shared between stages
37  property int shellOrientationAngle: 0
38  property int shellOrientation
39  property int shellPrimaryOrientation
40  property int nativeOrientation
41  property bool beingResized: false
42  property bool keepDashRunning: true
43  property bool suspended: false
44 
45  // functions to be called from outside
46  function updateFocusedAppOrientation() { /* TODO */ }
47  function updateFocusedAppOrientationAnimated() { /* TODO */}
48 
49  // To be read from outside
50  readonly property var mainApp: ApplicationManager.focusedApplicationId
51  ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
52  : null
53  property int mainAppWindowOrientationAngle: 0
54  readonly property bool orientationChangesEnabled: false
55 
56  property alias background: wallpaper.source
57  property bool altTabPressed: false
58 
59  CrossFadeImage {
60  id: wallpaper
61  anchors.fill: parent
62  sourceSize { height: root.height; width: root.width }
63  fillMode: Image.PreserveAspectCrop
64  }
65 
66  Connections {
67  target: ApplicationManager
68  onApplicationAdded: {
69  if (root.state == "altTab") {
70  root.state = "";
71  }
72 
73  ApplicationManager.requestFocusApplication(appId)
74  }
75 
76  onFocusRequested: {
77  var appIndex = priv.indexOf(appId);
78  var appDelegate = appRepeater.itemAt(appIndex);
79  appDelegate.minimized = false;
80  appDelegate.focus = true;
81  ApplicationManager.focusApplication(appId);
82  }
83  }
84 
85  QtObject {
86  id: priv
87 
88  readonly property string focusedAppId: ApplicationManager.focusedApplicationId
89  readonly property var focusedAppDelegate: {
90  var index = indexOf(focusedAppId);
91  return index >= 0 && index < appRepeater.count ? appRepeater.itemAt(index) : null
92  }
93 
94  onFocusedAppDelegateChanged: {
95  if (focusedAppDelegate) {
96  focusedAppDelegate.focus = true;
97  }
98  }
99 
100  function indexOf(appId) {
101  for (var i = 0; i < ApplicationManager.count; i++) {
102  if (ApplicationManager.get(i).appId == appId) {
103  return i;
104  }
105  }
106  return -1;
107  }
108  }
109 
110  Connections {
111  target: PanelState
112  onClose: {
113  ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
114  }
115  onMinimize: appRepeater.itemAt(0).minimize();
116  onMaximize: appRepeater.itemAt(0).unmaximize();
117  }
118 
119  Binding {
120  target: PanelState
121  property: "buttonsVisible"
122  value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
123  }
124 
125  Rectangle {
126  id: spreadBackground
127  anchors.fill: parent
128  color: "#55000000"
129  visible: false
130  }
131 
132  FocusScope {
133  id: appContainer
134  objectName: "appContainer"
135  anchors.fill: parent
136 
137  Keys.onPressed: {
138  switch (event.key) {
139  case Qt.Key_Left:
140  case Qt.Key_Backtab:
141  selectPrevious(event.isAutoRepeat)
142  break;
143  case Qt.Key_Right:
144  case Qt.Key_Tab:
145  selectNext(event.isAutoRepeat)
146  break;
147  case Qt.Key_Escape:
148  appRepeater.highlightedIndex = -1
149  case Qt.Key_Enter:
150  case Qt.Key_Return:
151  case Qt.Key_Space:
152  root.state = ""
153  }
154  }
155 
156  function selectNext(isAutoRepeat) {
157  if (isAutoRepeat && appRepeater.highlightedIndex >= ApplicationManager.count -1) {
158  return; // AutoRepeat is not allowed to wrap around
159  }
160 
161  appRepeater.highlightedIndex = (appRepeater.highlightedIndex + 1) % ApplicationManager.count;
162  var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 3));
163  if (spreadFlickable.contentX < newContentX || appRepeater.highlightedIndex == 0) {
164  spreadFlickable.snapTo(newContentX)
165  }
166  }
167 
168  function selectPrevious(isAutoRepeat) {
169  if (isAutoRepeat && appRepeater.highlightedIndex == 0) {
170  return; // AutoRepeat is not allowed to wrap around
171  }
172 
173  var newIndex = appRepeater.highlightedIndex - 1 >= 0 ? appRepeater.highlightedIndex - 1 : ApplicationManager.count - 1;
174  appRepeater.highlightedIndex = newIndex;
175  var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, appRepeater.highlightedIndex - 1));
176  if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
177  spreadFlickable.snapTo(newContentX)
178  }
179  }
180 
181  function focusSelected() {
182  if (appRepeater.highlightedIndex != -1) {
183  appRepeater.itemAt(appRepeater.highlightedIndex).focus = true;
184  }
185  }
186 
187  Repeater {
188  id: appRepeater
189  model: ApplicationManager
190  objectName: "appRepeater"
191 
192  property int highlightedIndex: -1
193  property int closingIndex: -1
194 
195  function indexOf(delegateItem) {
196  for (var i = 0; i < appRepeater.count; i++) {
197  if (appRepeater.itemAt(i) === delegateItem) {
198  return i;
199  }
200  }
201  return -1;
202  }
203 
204  delegate: FocusScope {
205  id: appDelegate
206  z: ApplicationManager.count - index
207  y: units.gu(3)
208  width: units.gu(60)
209  height: units.gu(50)
210 
211  property int windowWidth: 0
212  property int windowHeight: 0
213  // We don't want to resize the actual application when we're transforming things for the spread only
214  onWidthChanged: if (appDelegate.state !== "altTab") windowWidth = width
215  onHeightChanged: if (appDelegate.state !== "altTab") windowHeight = height
216 
217  readonly property int minWidth: units.gu(10)
218  readonly property int minHeight: units.gu(10)
219 
220  property bool maximized: false
221  property bool minimized: false
222 
223  onFocusChanged: {
224  if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
225  ApplicationManager.requestFocusApplication(model.appId);
226  decoratedWindow.forceActiveFocus();
227  }
228  }
229  Component.onCompleted: {
230  if (ApplicationManager.focusedApplicationId == model.appId) {
231  decoratedWindow.forceActiveFocus();
232  }
233  }
234 
235  Binding {
236  target: ApplicationManager.get(index)
237  property: "requestedState"
238  // TODO: figure out some lifecycle policy, like suspending minimized apps
239  // if running on a tablet or something.
240  // TODO: If the device has a dozen suspended apps because it was running
241  // in staged mode, when it switches to Windowed mode it will suddenly
242  // resume all those apps at once. We might want to avoid that.
243  value: ApplicationInfoInterface.RequestedRunning // Always running for now
244  }
245 
246  function maximize() {
247  minimized = false;
248  maximized = true;
249  }
250  function minimize() {
251  maximized = false;
252  minimized = true;
253  }
254  function unmaximize() {
255  minimized = false;
256  maximized = false;
257  }
258 
259  Behavior on x {
260  id: closeBehavior
261  enabled: appRepeater.closingIndex >= 0
262  UbuntuNumberAnimation {
263  onRunningChanged: if (!running) appRepeater.closingIndex = -1
264  }
265  }
266 
267  states: [
268  State {
269  name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized && root.state !== "altTab"
270  },
271  State {
272  name: "maximized"; when: appDelegate.maximized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
273  PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
274  },
275  State {
276  name: "minimized"; when: appDelegate.minimized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
277  PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
278  },
279  State {
280  name: "altTab"; when: root.state == "altTab" && root.workspacesUpdated
281  PropertyChanges {
282  target: appDelegate
283  x: spreadMaths.animatedX
284  y: spreadMaths.animatedY + (appDelegate.height - decoratedWindow.height) - units.gu(2)
285  width: spreadMaths.spreadHeight
286  height: spreadMaths.sceneHeight
287  angle: spreadMaths.animatedAngle
288  itemScale: spreadMaths.scale
289  itemScaleOriginY: decoratedWindow.height / 2;
290  z: index
291  visible: spreadMaths.itemVisible
292  }
293  PropertyChanges {
294  target: decoratedWindow
295  decorationShown: false
296  highlightShown: index == appRepeater.highlightedIndex
297  state: "transformed"
298  width: spreadMaths.spreadHeight
299  height: spreadMaths.spreadHeight
300  shadowOpacity: spreadMaths.shadowOpacity
301  anchors.topMargin: units.gu(2)
302  }
303  PropertyChanges {
304  target: tileInfo
305  visible: true
306  opacity: spreadMaths.tileInfoOpacity
307  }
308  PropertyChanges {
309  target: spreadSelectArea
310  enabled: true
311  }
312  PropertyChanges {
313  target: windowMoveResizeArea
314  enabled: false
315  }
316  }
317  ]
318  transitions: [
319  Transition {
320  from: "maximized,minimized,normal,"
321  to: "maximized,minimized,normal,"
322  PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
323  },
324  Transition {
325  from: ""
326  to: "altTab"
327  PropertyAction { target: appDelegate; properties: "y,angle,z,itemScale,itemScaleOriginY" }
328  PropertyAction { target: decoratedWindow; properties: "anchors.topMargin" }
329  PropertyAnimation {
330  target: appDelegate; properties: "x"
331  from: root.width
332  duration: rightEdgePushArea.containsMouse ? UbuntuAnimation.FastDuration :0
333  easing: UbuntuAnimation.StandardEasing
334  }
335  }
336  ]
337  property real angle: 0
338  property real itemScale: 1
339  property int itemScaleOriginX: 0
340  property int itemScaleOriginY: 0
341 
342  SpreadMaths {
343  id: spreadMaths
344  flickable: spreadFlickable
345  itemIndex: index
346  totalItems: Math.max(6, ApplicationManager.count)
347  sceneHeight: root.height
348  itemHeight: appDelegate.height
349  }
350 
351  WindowMoveResizeArea {
352  id: windowMoveResizeArea
353  target: appDelegate
354  minWidth: appDelegate.minWidth
355  minHeight: appDelegate.minHeight
356  resizeHandleWidth: units.gu(2)
357  windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
358 
359  onPressed: appDelegate.focus = true;
360  }
361 
362  DecoratedWindow {
363  id: decoratedWindow
364  objectName: "decoratedWindow"
365  anchors.left: appDelegate.left
366  anchors.top: appDelegate.top
367  windowWidth: appDelegate.windowWidth
368  windowHeight: appDelegate.windowHeight
369  application: ApplicationManager.get(index)
370  active: ApplicationManager.focusedApplicationId === model.appId
371  focus: false
372 
373  onClose: ApplicationManager.stopApplication(model.appId)
374  onMaximize: appDelegate.maximize()
375  onMinimize: appDelegate.minimize()
376 
377  transform: [
378  Scale {
379  origin.x: itemScaleOriginX
380  origin.y: itemScaleOriginY
381  xScale: itemScale
382  yScale: itemScale
383  },
384  Rotation {
385  origin { x: 0; y: (decoratedWindow.height - (decoratedWindow.height * itemScale / 2)) }
386  axis { x: 0; y: 1; z: 0 }
387  angle: appDelegate.angle
388  }
389  ]
390 
391  MouseArea {
392  id: spreadSelectArea
393  anchors.fill: parent
394  anchors.margins: -units.gu(2)
395  enabled: false
396  onClicked: {
397  appRepeater.highlightedIndex = index;
398  root.state = ""
399  }
400  }
401  }
402 
403  Image {
404  id: closeImage
405  anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset + units.gu(2) }
406  source: "graphics/window-close.svg"
407  readonly property var mousePos: hoverMouseArea.mapToItem(appDelegate, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
408  visible: index == appRepeater.highlightedIndex
409  && mousePos.y < (decoratedWindow.height / 3)
410  && mousePos.y > -units.gu(4)
411  && mousePos.x > -units.gu(4)
412  && mousePos.x < (decoratedWindow.width * 2 / 3)
413  height: units.gu(1.5)
414  width: height
415  sourceSize.width: width
416  sourceSize.height: height
417 
418  MouseArea {
419  id: closeMouseArea
420  objectName: "closeMouseArea"
421  anchors.fill: closeImage
422  anchors.margins: -units.gu(2)
423  onClicked: {
424  appRepeater.closingIndex = index;
425  ApplicationManager.stopApplication(model.appId)
426  }
427  }
428  }
429 
430  MouseArea {
431  id: tileInfo
432  objectName: "tileInfo"
433  anchors { left: parent.left; top: decoratedWindow.bottom; topMargin: units.gu(5) }
434  width: units.gu(30)
435  height: titleInfoColumn.height
436  visible: false
437  hoverEnabled: true
438 
439  onContainsMouseChanged: {
440  if (containsMouse) {
441  appRepeater.highlightedIndex = index
442  }
443  }
444 
445  onClicked: {
446  root.state = ""
447  }
448 
449  ColumnLayout {
450  id: titleInfoColumn
451  anchors { left: parent.left; top: parent.top; right: parent.right }
452  spacing: units.gu(1)
453 
454  UbuntuShape {
455  Layout.preferredHeight: Math.min(units.gu(6), root.height * .05)
456  Layout.preferredWidth: height * 8 / 7.6
457  image: Image {
458  anchors.fill: parent
459  source: model.icon
460  }
461  }
462  Label {
463  Layout.fillWidth: true
464  Layout.preferredHeight: units.gu(6)
465  text: model.name
466  wrapMode: Text.WordWrap
467  maximumLineCount: 2
468  }
469  }
470  }
471  }
472  }
473  }
474 
475  MouseArea {
476  id: hoverMouseArea
477  anchors.fill: appContainer
478  propagateComposedEvents: true
479  hoverEnabled: true
480  enabled: false
481 
482  property int scrollAreaWidth: root.width / 3
483  property bool progressiveScrollingEnabled: false
484 
485  onMouseXChanged: {
486  mouse.accepted = false
487  if (hoverMouseArea.pressed) return;
488 
489  // Find the hovered item and mark it active
490  var mapped = mapToItem(appContainer, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
491  var itemUnder = appContainer.childAt(mapped.x, mapped.y)
492  if (itemUnder) {
493  mapped = mapToItem(itemUnder, hoverMouseArea.mouseX, hoverMouseArea.mouseY)
494  var delegateChild = itemUnder.childAt(mapped.x, mapped.y)
495  if (delegateChild.objectName === "decoratedWindow" || delegateChild.objectName === "tileInfo") {
496  appRepeater.highlightedIndex = appRepeater.indexOf(itemUnder)
497  }
498  }
499 
500  if (spreadFlickable.contentWidth > spreadFlickable.minContentWidth) {
501  var margins = spreadFlickable.width * 0.05;
502 
503  if (!progressiveScrollingEnabled && mouseX < spreadFlickable.width - scrollAreaWidth) {
504  progressiveScrollingEnabled = true
505  }
506 
507  // do we need to scroll?
508  if (mouseX < scrollAreaWidth) {
509  var progress = Math.min(1, (scrollAreaWidth + margins - mouseX) / (scrollAreaWidth - margins));
510  var contentX = (1 - progress) * (spreadFlickable.contentWidth - spreadFlickable.width)
511  spreadFlickable.contentX = Math.max(0, Math.min(spreadFlickable.contentX, contentX))
512  }
513  if (mouseX > spreadFlickable.width - scrollAreaWidth && progressiveScrollingEnabled) {
514  var progress = Math.min(1, (mouseX - (spreadFlickable.width - scrollAreaWidth)) / (scrollAreaWidth - margins))
515  var contentX = progress * (spreadFlickable.contentWidth - spreadFlickable.width)
516  spreadFlickable.contentX = Math.min(spreadFlickable.contentWidth - spreadFlickable.width, Math.max(spreadFlickable.contentX, contentX))
517  }
518  }
519  }
520  onPressed: mouse.accepted = false
521  }
522 
523  FloatingFlickable {
524  id: spreadFlickable
525  objectName: "spreadFlickable"
526  anchors.fill: parent
527  property int minContentWidth: 6 * Math.min(height / 4, width / 5)
528  contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
529  enabled: false
530 
531  function snapTo(contentX) {
532  snapAnimation.stop();
533  snapAnimation.to = contentX
534  snapAnimation.start();
535  }
536 
537  UbuntuNumberAnimation {
538  id: snapAnimation
539  target: spreadFlickable
540  property: "contentX"
541  }
542  }
543 
544  Item {
545  id: workspaceSelector
546  anchors {
547  left: parent.left
548  top: parent.top
549  right: parent.right
550  topMargin: units.gu(3.5) // TODO: should be root.panelHeight
551  }
552  height: root.height * 0.25
553  visible: false
554 
555  RowLayout {
556  anchors.fill: parent
557  spacing: units.gu(1)
558  Item { Layout.fillWidth: true }
559  Repeater {
560  model: 1 // TODO: will be a workspacemodel in the future
561  Item {
562  Layout.fillHeight: true
563  Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
564  Image {
565  source: root.background
566  anchors {
567  left: parent.left
568  right: parent.right
569  verticalCenter: parent.verticalCenter
570  }
571  height: parent.height * 0.75
572 
573  // FIXME: This is temporary until we can have multiple Items per surface
574  ShaderEffect {
575  anchors.fill: parent
576 
577  property var source: ShaderEffectSource {
578  id: shaderEffectSource
579  live: false
580  sourceItem: appContainer
581  Connections { target: root; onUpdateWorkspaces: shaderEffectSource.scheduleUpdate() }
582  }
583 
584  fragmentShader: "
585  varying highp vec2 qt_TexCoord0;
586  uniform sampler2D source;
587  void main(void)
588  {
589  highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
590  gl_FragColor = sourceColor;
591  }"
592  }
593  }
594 
595  // TODO: This is the bar for the currently selected workspace
596  // Enable this once the workspace stuff is implemented
597 // Rectangle {
598 // anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
599 // height: units.dp(2)
600 // color: UbuntuColors.orange
601 // visible: index == 0 // TODO: should be active workspace index
602 // }
603  }
604 
605  }
606  // TODO: This is the "new workspace" button. Enable this once workspaces are implemented
607 // Item {
608 // Layout.fillHeight: true
609 // Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
610 // Rectangle {
611 // anchors {
612 // left: parent.left
613 // right: parent.right
614 // verticalCenter: parent.verticalCenter
615 // }
616 // height: parent.height * 0.75
617 // color: "#22ffffff"
618 
619 // Label {
620 // anchors.centerIn: parent
621 // font.pixelSize: parent.height / 2
622 // text: "+"
623 // }
624 // }
625 // }
626  Item { Layout.fillWidth: true }
627  }
628  }
629 
630  Label {
631  id: currentSelectedLabel
632  anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
633  text: appRepeater.highlightedIndex >= 0 ? ApplicationManager.get(appRepeater.highlightedIndex).name : ""
634  visible: false
635  fontSize: "large"
636  }
637 
638  states: [
639  State {
640  name: "altTab"; when: root.altTabPressed
641  PropertyChanges { target: workspaceSelector; visible: true }
642  PropertyChanges { target: spreadFlickable; enabled: spreadFlickable.contentWidth > spreadFlickable.minContentWidth }
643  PropertyChanges { target: currentSelectedLabel; visible: true }
644  PropertyChanges { target: spreadBackground; visible: true }
645  PropertyChanges { target: appContainer; focus: true }
646  PropertyChanges { target: hoverMouseArea; enabled: true }
647  }
648  ]
649  signal updateWorkspaces();
650  property bool workspacesUpdated: false
651  transitions: [
652  Transition {
653  from: "*"
654  to: "altTab"
655  SequentialAnimation {
656  PropertyAction { target: hoverMouseArea; property: "progressiveScrollingEnabled"; value: false }
657  PropertyAction { target: appRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
658  PauseAnimation { duration: 50 }
659  PropertyAction { target: workspaceSelector; property: "visible" }
660  ScriptAction { script: root.updateWorkspaces() }
661  // FIXME: Updating of shaderEffectSource take a bit of time. This is temporary until we can paint multiple items per surface
662  PauseAnimation { duration: 10 }
663  PropertyAction { target: root; property: "workspacesUpdated"; value: true }
664  PropertyAction { target: spreadFlickable; property: "visible" }
665  PropertyAction { targets: [currentSelectedLabel,spreadBackground]; property: "visible" }
666  PropertyAction { target: spreadFlickable; property: "contentX"; value: 0 }
667  }
668  },
669  Transition {
670  from: "*"
671  to: "*"
672  PropertyAnimation { property: "opacity" }
673  PropertyAction { target: root; property: "workspacesUpdated"; value: false }
674  ScriptAction { script: { appContainer.focusSelected() } }
675  PropertyAction { target: appRepeater; property: "highlightedIndex"; value: -1 }
676  }
677 
678  ]
679 
680  MouseArea {
681  id: rightEdgePushArea
682  anchors {
683  top: parent.top
684  right: parent.right
685  bottom: parent.bottom
686  }
687  // TODO: Make this a push to edge thing like the launcher when we can,
688  // for now, yes, we want 1 pixel, regardless of the scaling
689  width: 1
690  hoverEnabled: true
691  onContainsMouseChanged: {
692  if (containsMouse) {
693  root.state = "altTab";
694  }
695  }
696  }
697 }