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.3
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  delegate: FocusScope {
196  id: appDelegate
197  z: ApplicationManager.count - index
198  y: units.gu(3)
199  width: units.gu(60)
200  height: units.gu(50)
201 
202  readonly property int minWidth: units.gu(10)
203  readonly property int minHeight: units.gu(10)
204 
205  property bool maximized: false
206  property bool minimized: false
207 
208  onFocusChanged: {
209  if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
210  ApplicationManager.requestFocusApplication(model.appId);
211  decoratedWindow.forceActiveFocus();
212  }
213  }
214  Component.onCompleted: {
215  if (ApplicationManager.focusedApplicationId == model.appId) {
216  decoratedWindow.forceActiveFocus();
217  }
218  }
219 
220  Binding {
221  target: ApplicationManager.get(index)
222  property: "requestedState"
223  // TODO: figure out some lifecycle policy, like suspending minimized apps
224  // if running on a tablet or something.
225  // TODO: If the device has a dozen suspended apps because it was running
226  // in staged mode, when it switches to Windowed mode it will suddenly
227  // resume all those apps at once. We might want to avoid that.
228  value: ApplicationInfoInterface.RequestedRunning // Always running for now
229  }
230 
231  function maximize() {
232  minimized = false;
233  maximized = true;
234  }
235  function minimize() {
236  maximized = false;
237  minimized = true;
238  }
239  function unmaximize() {
240  minimized = false;
241  maximized = false;
242  }
243 
244  Behavior on x {
245  id: closeBehavior
246  enabled: appRepeater.closingIndex >= 0
247  UbuntuNumberAnimation {
248  onRunningChanged: if (!running) appRepeater.closingIndex = -1
249  }
250  }
251 
252  states: [
253  State {
254  name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized && root.state !== "altTab"
255  },
256  State {
257  name: "maximized"; when: appDelegate.maximized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
258  PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
259  },
260  State {
261  name: "minimized"; when: appDelegate.minimized && (root.state !== "altTab" || (root.state == "altTab" && !root.workspacesUpdated))
262  PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
263  },
264  State {
265  name: "altTab"; when: root.state == "altTab" && root.workspacesUpdated
266  PropertyChanges {
267  target: appDelegate
268  x: spreadMaths.animatedX
269  y: spreadMaths.animatedY + (appDelegate.height - decoratedWindow.height)
270  angle: spreadMaths.animatedAngle
271  itemScale: spreadMaths.scale
272  itemScaleOriginY: decoratedWindow.height / 2;
273  z: index
274  visible: spreadMaths.itemVisible
275  }
276  PropertyChanges {
277  target: decoratedWindow
278  decorationShown: false
279  highlightShown: index == appRepeater.highlightedIndex
280  state: "transformed"
281  width: spreadMaths.spreadHeight
282  height: spreadMaths.spreadHeight
283  shadowOpacity: spreadMaths.shadowOpacity
284  }
285  PropertyChanges {
286  target: tileInfo
287  visible: true
288  opacity: spreadMaths.tileInfoOpacity
289  }
290  PropertyChanges {
291  target: spreadSelectArea
292  enabled: true
293  }
294  PropertyChanges {
295  target: windowMoveResizeArea
296  enabled: false
297  }
298  }
299  ]
300  transitions: [
301  Transition {
302  from: "maximized,minimized,normal,"
303  to: "maximized,minimized,normal,"
304  PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
305  }
306  ]
307  property real angle: 0
308  property real itemScale: 1
309  property int itemScaleOriginX: 0
310  property int itemScaleOriginY: 0
311 
312  SpreadMaths {
313  id: spreadMaths
314  flickable: spreadFlickable
315  itemIndex: index
316  totalItems: Math.max(6, ApplicationManager.count)
317  sceneHeight: root.height
318  itemHeight: appDelegate.height
319  }
320 
321  WindowMoveResizeArea {
322  id: windowMoveResizeArea
323  target: appDelegate
324  minWidth: appDelegate.minWidth
325  minHeight: appDelegate.minHeight
326  resizeHandleWidth: units.gu(2)
327  windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
328 
329  onPressed: appDelegate.focus = true;
330  }
331 
332  DecoratedWindow {
333  id: decoratedWindow
334  objectName: "decoratedWindow"
335  anchors.left: appDelegate.left
336  anchors.top: appDelegate.top
337  windowWidth: appDelegate.width
338  windowHeight: appDelegate.height
339  application: ApplicationManager.get(index)
340  active: ApplicationManager.focusedApplicationId === model.appId
341  focus: false
342 
343  onClose: ApplicationManager.stopApplication(model.appId)
344  onMaximize: appDelegate.maximize()
345  onMinimize: appDelegate.minimize()
346 
347  transform: [
348  Scale {
349  origin.x: itemScaleOriginX
350  origin.y: itemScaleOriginY
351  xScale: itemScale
352  yScale: itemScale
353  },
354  Rotation {
355  origin { x: 0; y: (decoratedWindow.height - (decoratedWindow.height * itemScale / 2)) }
356  axis { x: 0; y: 1; z: 0 }
357  angle: appDelegate.angle
358  }
359  ]
360 
361  MouseArea {
362  id: spreadSelectArea
363  anchors.fill: parent
364  anchors.margins: -units.gu(2)
365  enabled: false
366  hoverEnabled: enabled
367 
368  // There is a bug in MouseArea where containsMouse doesn't
369  // return to false if the MouseArea is disabled while
370  // containing the mouse. Let's manage the property our own.
371  property bool upperThirdContainsMouse: false
372  onContainsMouseChanged: evaluateContainsMouse()
373  onMouseYChanged: evaluateContainsMouse()
374  function evaluateContainsMouse() {
375  if (containsMouse) {
376  appRepeater.highlightedIndex = index
377  }
378 
379  if (containsMouse && mouseY < height / 3) {
380  spreadSelectArea.upperThirdContainsMouse = true
381  } else {
382  spreadSelectArea.upperThirdContainsMouse = false;
383  }
384  }
385  onEnabledChanged: {
386  if (!enabled) {
387  spreadSelectArea.upperThirdContainsMouse = false
388  }
389  }
390 
391  onClicked: {
392  root.state = ""
393  }
394  }
395  }
396 
397  Image {
398  id: closeImage
399  anchors { left: parent.left; top: parent.top; leftMargin: -height / 2; topMargin: -height / 2 + spreadMaths.closeIconOffset }
400  source: "graphics/window-close.svg"
401  visible: spreadSelectArea.upperThirdContainsMouse
402  height: units.gu(1.5)
403  width: height
404  sourceSize.width: width
405  sourceSize.height: height
406  }
407 
408  MouseArea {
409  id: closeMouseArea
410  objectName: "closeMouseArea"
411  anchors.fill: closeImage
412  anchors.margins: -units.gu(2)
413  enabled: spreadSelectArea.upperThirdContainsMouse
414  onClicked: {
415  appRepeater.closingIndex = index;
416  ApplicationManager.stopApplication(model.appId)
417  }
418  }
419 
420  MouseArea {
421  id: tileInfo
422  objectName: "tileInfo"
423  anchors { left: parent.left; top: decoratedWindow.bottom; topMargin: units.gu(5) }
424  width: units.gu(30)
425  height: titleInfoColumn.height
426  visible: false
427  hoverEnabled: true
428 
429  onContainsMouseChanged: {
430  if (containsMouse) {
431  appRepeater.highlightedIndex = index
432  }
433  }
434 
435  onClicked: {
436  root.state = ""
437  }
438 
439  ColumnLayout {
440  id: titleInfoColumn
441  anchors { left: parent.left; top: parent.top; right: parent.right }
442  spacing: units.gu(1)
443 
444  UbuntuShape {
445  Layout.preferredHeight: Math.min(units.gu(6), root.height * .05)
446  Layout.preferredWidth: height * 8 / 7.6
447  image: Image {
448  anchors.fill: parent
449  source: model.icon
450  }
451  }
452  Label {
453  Layout.fillWidth: true
454  Layout.preferredHeight: units.gu(6)
455  text: model.name
456  wrapMode: Text.WordWrap
457  maximumLineCount: 2
458  }
459  }
460  }
461  }
462  }
463  }
464 
465  FloatingFlickable {
466  id: spreadFlickable
467  anchors.fill: parent
468  contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
469  enabled: false
470 
471  function snapTo(contentX) {
472  snapAnimation.stop();
473  snapAnimation.to = contentX
474  snapAnimation.start();
475  }
476 
477  UbuntuNumberAnimation {
478  id: snapAnimation
479  target: spreadFlickable
480  property: "contentX"
481  }
482  }
483 
484  Item {
485  id: workspaceSelector
486  anchors {
487  left: parent.left
488  top: parent.top
489  right: parent.right
490  topMargin: units.gu(3.5) // TODO: should be root.panelHeight
491  }
492  height: root.height * 0.25
493  visible: false
494 
495  RowLayout {
496  anchors.fill: parent
497  spacing: units.gu(1)
498  Item { Layout.fillWidth: true }
499  Repeater {
500  model: 1 // TODO: will be a workspacemodel in the future
501  Item {
502  Layout.fillHeight: true
503  Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
504  Image {
505  source: root.background
506  anchors {
507  left: parent.left
508  right: parent.right
509  verticalCenter: parent.verticalCenter
510  }
511  height: parent.height * 0.75
512 
513  // FIXME: This is temporary until we can have multiple Items per surface
514  ShaderEffect {
515  anchors.fill: parent
516 
517  property var source: ShaderEffectSource {
518  id: shaderEffectSource
519  live: false
520  sourceItem: appContainer
521  Connections { target: root; onUpdateWorkspaces: shaderEffectSource.scheduleUpdate() }
522  }
523 
524  fragmentShader: "
525  varying highp vec2 qt_TexCoord0;
526  uniform sampler2D source;
527  void main(void)
528  {
529  highp vec4 sourceColor = texture2D(source, qt_TexCoord0);
530  gl_FragColor = sourceColor;
531  }"
532  }
533  }
534 
535  // TODO: This is the bar for the currently selected workspace
536  // Enable this once the workspace stuff is implemented
537 // Rectangle {
538 // anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
539 // height: units.dp(2)
540 // color: UbuntuColors.orange
541 // visible: index == 0 // TODO: should be active workspace index
542 // }
543  }
544 
545  }
546  // TODO: This is the "new workspace" button. Enable this once workspaces are implemented
547 // Item {
548 // Layout.fillHeight: true
549 // Layout.preferredWidth: ((height - units.gu(6)) * root.width / root.height)
550 // Rectangle {
551 // anchors {
552 // left: parent.left
553 // right: parent.right
554 // verticalCenter: parent.verticalCenter
555 // }
556 // height: parent.height * 0.75
557 // color: "#22ffffff"
558 
559 // Label {
560 // anchors.centerIn: parent
561 // font.pixelSize: parent.height / 2
562 // text: "+"
563 // }
564 // }
565 // }
566  Item { Layout.fillWidth: true }
567  }
568  }
569 
570  Label {
571  id: currentSelectedLabel
572  anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
573  text: appRepeater.highlightedIndex >= 0 ? ApplicationManager.get(appRepeater.highlightedIndex).name : ""
574  visible: false
575  fontSize: "large"
576  }
577 
578  states: [
579  State {
580  name: "altTab"; when: root.altTabPressed
581  PropertyChanges { target: workspaceSelector; visible: true }
582  PropertyChanges { target: spreadFlickable; enabled: true }
583  PropertyChanges { target: currentSelectedLabel; visible: true }
584  PropertyChanges { target: spreadBackground; visible: true }
585  PropertyChanges { target: appContainer; focus: true }
586  }
587  ]
588  signal updateWorkspaces();
589  property bool workspacesUpdated: false
590  transitions: [
591  Transition {
592  from: "*"
593  to: "altTab"
594  SequentialAnimation {
595  PropertyAction { target: appRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
596  PauseAnimation { duration: 50 }
597  PropertyAction { target: workspaceSelector; property: "visible" }
598  ScriptAction { script: root.updateWorkspaces() }
599  // FIXME: Updating of shaderEffectSource take a bit of time. This is temporary until we can paint multiple items per surface
600  PauseAnimation { duration: 10 }
601  PropertyAction { target: root; property: "workspacesUpdated"; value: true }
602  PropertyAction { target: spreadFlickable; property: "visible" }
603  PropertyAction { targets: [currentSelectedLabel,spreadBackground]; property: "visible" }
604  PropertyAction { target: spreadFlickable; property: "contentX"; value: 0 }
605  }
606  },
607  Transition {
608  from: "*"
609  to: "*"
610  PropertyAnimation { property: "opacity" }
611  PropertyAction { target: root; property: "workspacesUpdated"; value: false }
612  ScriptAction { script: { appContainer.focusSelected() } }
613  PropertyAction { target: appRepeater; property: "highlightedIndex"; value: -1 }
614  }
615 
616  ]
617 
618  MouseArea {
619  anchors {
620  top: parent.top
621  right: parent.right
622  bottom: parent.bottom
623  }
624  // TODO: Make this a push to edge thing like the launcher when we can,
625  // for now, yes, we want 1 pixel, regardless of the scaling
626  width: 1
627  hoverEnabled: true
628  onContainsMouseChanged: {
629  if (containsMouse) {
630  root.state = "altTab"
631  }
632  }
633  }
634 }