Unity 8
DesktopStage.qml
1 /*
2  * Copyright (C) 2014-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 Ubuntu.Components 1.3
19 import Unity.Application 0.1
20 import "../Components/PanelState"
21 import "../Components"
22 import Utils 0.1
23 import Ubuntu.Gestures 0.1
24 import GlobalShortcut 1.0
25 
26 AbstractStage {
27  id: root
28  anchors.fill: parent
29  paintBackground: false
30 
31  // functions to be called from outside
32  function updateFocusedAppOrientation() { /* TODO */ }
33  function updateFocusedAppOrientationAnimated() { /* TODO */}
34  function pushRightEdge(amount) {
35  if (spread.state === "") {
36  edgeBarrier.push(amount);
37  }
38  }
39 
40  // Used by TutorialRight
41  property bool spreadShown: spread.state == "altTab"
42 
43  mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
44 
45  // application windows never rotate independently
46  mainAppWindowOrientationAngle: shellOrientationAngle
47 
48  orientationChangesEnabled: true
49 
50  GlobalShortcut {
51  id: closeWindowShortcut
52  shortcut: Qt.AltModifier|Qt.Key_F4
53  onTriggered: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
54  active: priv.focusedAppDelegate !== null
55  }
56 
57  GlobalShortcut {
58  id: showSpreadShortcut
59  shortcut: Qt.MetaModifier|Qt.Key_W
60  onTriggered: spread.state = "altTab"
61  }
62 
63  GlobalShortcut {
64  id: minimizeAllShortcut
65  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_D
66  onTriggered: priv.minimizeAllWindows()
67  }
68 
69  GlobalShortcut {
70  id: maximizeWindowShortcut
71  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Up
72  onTriggered: priv.focusedAppDelegate.maximize()
73  active: priv.focusedAppDelegate !== null
74  }
75 
76  GlobalShortcut {
77  id: maximizeWindowLeftShortcut
78  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Left
79  onTriggered: priv.focusedAppDelegate.maximizeLeft()
80  active: priv.focusedAppDelegate !== null
81  }
82 
83  GlobalShortcut {
84  id: maximizeWindowRightShortcut
85  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Right
86  onTriggered: priv.focusedAppDelegate.maximizeRight()
87  active: priv.focusedAppDelegate !== null
88  }
89 
90  GlobalShortcut {
91  id: minimizeRestoreShortcut
92  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Down
93  onTriggered: priv.focusedAppDelegate.maximized || priv.focusedAppDelegate.maximizedLeft || priv.focusedAppDelegate.maximizedRight ||
94  priv.focusedAppDelegate.maximizedHorizontally || priv.focusedAppDelegate.maximizedVertically
95  ? priv.focusedAppDelegate.restoreFromMaximized() : priv.focusedAppDelegate.minimize()
96  active: priv.focusedAppDelegate !== null
97  }
98 
99  GlobalShortcut {
100  shortcut: Qt.AltModifier|Qt.Key_Print
101  onTriggered: root.itemSnapshotRequested(priv.focusedAppDelegate)
102  active: priv.focusedAppDelegate !== null
103  }
104 
105  Connections {
106  target: root.topLevelSurfaceList
107  onCountChanged: {
108  if (spread.state == "altTab") {
109  spread.cancel();
110  }
111  }
112  }
113 
114  QtObject {
115  id: priv
116  objectName: "DesktopStagePrivate"
117 
118  property var focusedAppDelegate: null
119  onFocusedAppDelegateChanged: {
120  if (spread.state == "altTab") {
121  spread.state = "";
122  }
123  }
124 
125  property var foregroundMaximizedAppDelegate: null // for stuff like drop shadow and focusing maximized app by clicking panel
126 
127  function updateForegroundMaximizedApp() {
128  var found = false;
129  for (var i = 0; i < appRepeater.count && !found; i++) {
130  var item = appRepeater.itemAt(i);
131  if (item && item.visuallyMaximized) {
132  foregroundMaximizedAppDelegate = item;
133  found = true;
134  }
135  }
136  if (!found) {
137  foregroundMaximizedAppDelegate = null;
138  }
139  }
140 
141  function minimizeAllWindows() {
142  for (var i = 0; i < appRepeater.count; i++) {
143  var appDelegate = appRepeater.itemAt(i);
144  if (appDelegate && !appDelegate.minimized) {
145  appDelegate.minimize();
146  }
147  }
148  }
149 
150  function focusNext() {
151  for (var i = 0; i < appRepeater.count; i++) {
152  var appDelegate = appRepeater.itemAt(i);
153  if (appDelegate && !appDelegate.minimized) {
154  appDelegate.focus = true;
155  return;
156  }
157  }
158  }
159  }
160 
161  Connections {
162  target: PanelState
163  onCloseClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
164  onMinimizeClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.minimize(); } }
165  onRestoreClicked: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.restoreFromMaximized(); } }
166  onFocusMaximizedApp: {
167  if (priv.foregroundMaximizedAppDelegate) {
168  priv.foregroundMaximizedAppDelegate.focus = true;
169  }
170  }
171  }
172 
173  Binding {
174  target: PanelState
175  property: "buttonsVisible"
176  value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized // FIXME for Locally integrated menus
177  && spread.state == ""
178  }
179 
180  Binding {
181  target: PanelState
182  property: "title"
183  value: {
184  if (priv.focusedAppDelegate !== null && spread.state == "") {
185  if (priv.focusedAppDelegate.maximized)
186  return priv.focusedAppDelegate.title
187  else
188  return priv.focusedAppDelegate.appName
189  }
190  return ""
191  }
192  when: priv.focusedAppDelegate
193  }
194 
195  Binding {
196  target: PanelState
197  property: "dropShadow"
198  value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppDelegate !== null
199  }
200 
201  Binding {
202  target: PanelState
203  property: "closeButtonShown"
204  value: priv.focusedAppDelegate && priv.focusedAppDelegate.maximized && priv.focusedAppDelegate.application.appId !== "unity8-dash"
205  }
206 
207  Component.onDestruction: {
208  PanelState.title = "";
209  PanelState.buttonsVisible = false;
210  PanelState.dropShadow = false;
211  }
212 
213  Instantiator {
214  model: root.applicationManager
215  delegate: Binding {
216  target: model.application
217  property: "requestedState"
218 
219  // TODO: figure out some lifecycle policy, like suspending minimized apps
220  // if running on a tablet or something.
221  // TODO: If the device has a dozen suspended apps because it was running
222  // in staged mode, when it switches to Windowed mode it will suddenly
223  // resume all those apps at once. We might want to avoid that.
224  value: ApplicationInfoInterface.RequestedRunning // Always running for now
225  }
226  }
227 
228  Binding {
229  target: MirFocusController
230  property: "focusedSurface"
231  value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
232  when: !appRepeater.startingUp && root.parent
233  }
234 
235  FocusScope {
236  id: appContainer
237  objectName: "appContainer"
238  anchors.fill: parent
239  focus: spread.state !== "altTab"
240 
241  CrossFadeImage {
242  id: wallpaper
243  anchors.fill: parent
244  source: root.background
245  sourceSize { height: root.height; width: root.width }
246  fillMode: Image.PreserveAspectCrop
247  }
248 
249  TopLevelSurfaceRepeater {
250  id: appRepeater
251  model: topLevelSurfaceList
252  objectName: "appRepeater"
253 
254  delegate: FocusScope {
255  id: appDelegate
256  objectName: "appDelegate_" + model.id
257  // z might be overriden in some cases by effects, but we need z ordering
258  // to calculate occlusion detection
259  property int normalZ: topLevelSurfaceList.count - index
260  onNormalZChanged: {
261  if (visuallyMaximized) {
262  priv.updateForegroundMaximizedApp();
263  }
264  }
265  z: normalZ
266  x: priv.focusedAppDelegate ? priv.focusedAppDelegate.x + units.gu(3) : (normalZ - 1) * units.gu(3)
267  y: priv.focusedAppDelegate ? priv.focusedAppDelegate.y + units.gu(3) : normalZ * units.gu(3)
268 
269  width: decoratedWindow.width
270  height: decoratedWindow.height
271  property int requestedWidth: -1
272  property int requestedHeight: -1
273  property alias minimumWidth: decoratedWindow.minimumWidth
274  property alias minimumHeight: decoratedWindow.minimumHeight
275  property alias maximumWidth: decoratedWindow.maximumWidth
276  property alias maximumHeight: decoratedWindow.maximumHeight
277  property alias widthIncrement: decoratedWindow.widthIncrement
278  property alias heightIncrement: decoratedWindow.heightIncrement
279 
280  readonly property bool maximized: windowState & WindowStateStorage.WindowStateMaximized
281  readonly property bool maximizedLeft: windowState & WindowStateStorage.WindowStateMaximizedLeft
282  readonly property bool maximizedRight: windowState & WindowStateStorage.WindowStateMaximizedRight
283  readonly property bool maximizedHorizontally: windowState & WindowStateStorage.WindowStateMaximizedHorizontally
284  readonly property bool maximizedVertically: windowState & WindowStateStorage.WindowStateMaximizedVertically
285  readonly property bool minimized: windowState & WindowStateStorage.WindowStateMinimized
286  readonly property alias fullscreen: decoratedWindow.fullscreen
287 
288  property int windowState: WindowStateStorage.WindowStateNormal
289 
290  readonly property var application: model.application
291  property bool animationsEnabled: true
292  property alias title: decoratedWindow.title
293  readonly property string appName: model.application ? model.application.name : ""
294  property bool visuallyMaximized: false
295  property bool visuallyMinimized: false
296 
297  readonly property var surface: model.surface
298 
299  function claimFocus() {
300  if (spread.state == "altTab") {
301  spread.cancel();
302  }
303  appDelegate.restore();
304  }
305  Connections {
306  target: model.surface
307  onFocusRequested: claimFocus();
308  }
309  Connections {
310  target: model.application
311  onFocusRequested: {
312  if (!model.surface) {
313  // when an app has no surfaces, we assume there's only one entry representing it:
314  // this delegate.
315  claimFocus();
316  } else {
317  // if the application has surfaces, focus request should be at surface-level.
318  }
319  }
320  }
321 
322  onFocusChanged: {
323  if (appRepeater.startingUp)
324  return;
325 
326  if (focus) {
327  priv.focusedAppDelegate = appDelegate;
328 
329  // If we're orphan (!parent) it means this stage is no longer the current one
330  // and will be deleted shortly. So we should no longer have a say over the model
331  if (root.parent) {
332  topLevelSurfaceList.raiseId(model.id);
333  }
334  } else if (!focus && priv.focusedAppDelegate === appDelegate) {
335  priv.focusedAppDelegate = null;
336  // FIXME: No idea why the Binding{} doens't update when focusedAppDelegate turns null
337  MirFocusController.focusedSurface = null;
338  }
339  }
340  Component.onCompleted: {
341  // NB: We're differentiating if this delegate was created in response to a new entry in the model
342  // or if the Repeater is just populating itself with delegates to match the model it received.
343  if (!appRepeater.startingUp) {
344  // a top level window is always the focused one when it first appears, unfocusing
345  // any preexisting one
346  focus = true;
347  }
348  }
349  Component.onDestruction: {
350  if (!root.parent) {
351  // This stage is about to be destroyed. Don't mess up with the model at this point
352  return;
353  }
354 
355  if (visuallyMaximized) {
356  priv.updateForegroundMaximizedApp();
357  }
358 
359  if (focus) {
360  // focus some other window
361  for (var i = 0; i < appRepeater.count; i++) {
362  var appDelegate = appRepeater.itemAt(i);
363  if (appDelegate && !appDelegate.minimized && i != index) {
364  appDelegate.focus = true;
365  return;
366  }
367  }
368  }
369  }
370 
371  onVisuallyMaximizedChanged: priv.updateForegroundMaximizedApp()
372 
373  visible: (
374  !visuallyMinimized
375  && !greeter.fullyShown
376  && (priv.foregroundMaximizedAppDelegate === null || priv.foregroundMaximizedAppDelegate.normalZ <= z)
377  )
378  || decoratedWindow.fullscreen
379  || (spread.state == "altTab" && index === spread.highlightedIndex)
380 
381  function close() {
382  model.surface.close();
383  }
384 
385  function maximize(animated) {
386  animationsEnabled = (animated === undefined) || animated;
387  windowState = WindowStateStorage.WindowStateMaximized;
388  }
389  function maximizeLeft(animated) {
390  animationsEnabled = (animated === undefined) || animated;
391  windowState = WindowStateStorage.WindowStateMaximizedLeft;
392  }
393  function maximizeRight(animated) {
394  animationsEnabled = (animated === undefined) || animated;
395  windowState = WindowStateStorage.WindowStateMaximizedRight;
396  }
397  function maximizeHorizontally(animated) {
398  animationsEnabled = (animated === undefined) || animated;
399  windowState = WindowStateStorage.WindowStateMaximizedHorizontally;
400  }
401  function maximizeVertically(animated) {
402  animationsEnabled = (animated === undefined) || animated;
403  windowState = WindowStateStorage.WindowStateMaximizedVertically;
404  }
405  function minimize(animated) {
406  animationsEnabled = (animated === undefined) || animated;
407  windowState |= WindowStateStorage.WindowStateMinimized; // add the minimized bit
408  }
409  function restoreFromMaximized(animated) {
410  animationsEnabled = (animated === undefined) || animated;
411  windowState = WindowStateStorage.WindowStateNormal;
412  }
413  function restore(animated) {
414  animationsEnabled = (animated === undefined) || animated;
415  windowState &= ~WindowStateStorage.WindowStateMinimized; // clear the minimized bit
416  if (maximized)
417  maximize();
418  else if (maximizedLeft)
419  maximizeLeft();
420  else if (maximizedRight)
421  maximizeRight();
422  else if (maximizedHorizontally)
423  maximizeHorizontally();
424  else if (maximizedVertically)
425  maximizeVertically();
426 
427  focus = true;
428  }
429 
430  function playFocusAnimation() {
431  focusAnimation.start()
432  }
433 
434  UbuntuNumberAnimation {
435  id: focusAnimation
436  target: appDelegate
437  property: "scale"
438  from: 0.98
439  to: 1
440  duration: UbuntuAnimation.SnapDuration
441  }
442 
443  states: [
444  State {
445  name: "fullscreen"; when: decoratedWindow.fullscreen && !appDelegate.minimized
446  PropertyChanges {
447  target: appDelegate;
448  x: 0;
449  y: -PanelState.panelHeight
450  requestedWidth: appContainer.width;
451  requestedHeight: appContainer.height;
452  }
453  },
454  State {
455  name: "normal";
456  when: appDelegate.windowState == WindowStateStorage.WindowStateNormal
457  PropertyChanges {
458  target: appDelegate;
459  visuallyMinimized: false;
460  visuallyMaximized: false
461  }
462  },
463  State {
464  name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
465  PropertyChanges {
466  target: appDelegate;
467  x: root.leftMargin;
468  y: 0;
469  visuallyMinimized: false;
470  visuallyMaximized: true
471  }
472  PropertyChanges {
473  target: decoratedWindow
474  requestedWidth: appContainer.width - root.leftMargin;
475  requestedHeight: appContainer.height;
476  }
477  },
478  State {
479  name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
480  PropertyChanges {
481  target: appDelegate
482  x: root.leftMargin
483  y: PanelState.panelHeight
484  }
485  PropertyChanges {
486  target: decoratedWindow
487  requestedWidth: (appContainer.width - root.leftMargin)/2
488  requestedHeight: appContainer.height - PanelState.panelHeight
489  }
490  },
491  State {
492  name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
493  PropertyChanges {
494  target: appDelegate;
495  x: (appContainer.width + root.leftMargin)/2
496  y: PanelState.panelHeight
497  }
498  PropertyChanges {
499  target: decoratedWindow
500  requestedWidth: (appContainer.width - root.leftMargin)/2
501  requestedHeight: appContainer.height - PanelState.panelHeight
502  }
503  },
504  State {
505  name: "maximizedHorizontally"; when: appDelegate.maximizedHorizontally && !appDelegate.minimized
506  PropertyChanges { target: appDelegate; x: root.leftMargin }
507  PropertyChanges { target: decoratedWindow; requestedWidth: appContainer.width - root.leftMargin }
508  },
509  State {
510  name: "maximizedVertically"; when: appDelegate.maximizedVertically && !appDelegate.minimized
511  PropertyChanges { target: appDelegate; y: PanelState.panelHeight }
512  PropertyChanges { target: decoratedWindow; requestedHeight: appContainer.height - PanelState.panelHeight }
513  },
514  State {
515  name: "minimized"; when: appDelegate.minimized
516  PropertyChanges {
517  target: appDelegate;
518  x: -appDelegate.width / 2;
519  scale: units.gu(5) / appDelegate.width;
520  opacity: 0;
521  visuallyMinimized: true;
522  visuallyMaximized: false
523  }
524  }
525  ]
526  transitions: [
527  Transition {
528  to: "normal"
529  enabled: appDelegate.animationsEnabled
530  PropertyAction { target: appDelegate; properties: "visuallyMinimized,visuallyMaximized" }
531  UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
532  UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
533  },
534  Transition {
535  to: "minimized"
536  enabled: appDelegate.animationsEnabled
537  PropertyAction { target: appDelegate; property: "visuallyMaximized" }
538  SequentialAnimation {
539  ParallelAnimation {
540  UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
541  UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
542  }
543  PropertyAction { target: appDelegate; property: "visuallyMinimized" }
544  ScriptAction {
545  script: {
546  if (appDelegate.minimized) {
547  appDelegate.focus = false;
548  priv.focusNext();
549  }
550  }
551  }
552  }
553  },
554  Transition {
555  to: "*" //maximized and fullscreen
556  enabled: appDelegate.animationsEnabled
557  PropertyAction { target: appDelegate; property: "visuallyMinimized" }
558  SequentialAnimation {
559  ParallelAnimation {
560  UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
561  UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
562  }
563  PropertyAction { target: appDelegate; property: "visuallyMaximized" }
564  }
565  }
566  ]
567 
568  Binding {
569  id: previewBinding
570  target: appDelegate
571  property: "z"
572  value: topLevelSurfaceList.count + 1
573  when: index == spread.highlightedIndex && spread.ready
574  }
575 
576  WindowResizeArea {
577  id: resizeArea
578  objectName: "windowResizeArea"
579  target: appDelegate
580  minWidth: units.gu(10)
581  minHeight: units.gu(10)
582  borderThickness: units.gu(2)
583  windowId: model.application.appId // FIXME: Change this to point to windowId once we have such a thing
584  screenWidth: appContainer.width
585  screenHeight: appContainer.height
586  leftMargin: root.leftMargin
587 
588  onPressed: { appDelegate.focus = true; }
589 
590  Component.onCompleted: {
591  loadWindowState();
592  }
593 
594  property bool saveStateOnDestruction: true
595  Connections {
596  target: root
597  onStageAboutToBeUnloaded: {
598  resizeArea.saveWindowState();
599  resizeArea.saveStateOnDestruction = false;
600  fullscreenPolicy.active = false;
601  }
602  }
603  Component.onDestruction: {
604  if (saveStateOnDestruction) {
605  saveWindowState();
606  }
607  }
608  }
609 
610  DecoratedWindow {
611  id: decoratedWindow
612  objectName: "decoratedWindow"
613  anchors.left: appDelegate.left
614  anchors.top: appDelegate.top
615  application: model.application
616  surface: model.surface
617  active: appDelegate.focus
618  focus: true
619 
620  requestedWidth: appDelegate.requestedWidth
621  requestedHeight: appDelegate.requestedHeight
622 
623  onCloseClicked: { appDelegate.close(); }
624  onMaximizeClicked: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
625  || appDelegate.maximizedHorizontally || appDelegate.maximizedVertically
626  ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
627  onMaximizeHorizontallyClicked: appDelegate.maximizedHorizontally ? appDelegate.restoreFromMaximized() : appDelegate.maximizeHorizontally()
628  onMaximizeVerticallyClicked: appDelegate.maximizedVertically ? appDelegate.restoreFromMaximized() : appDelegate.maximizeVertically()
629  onMinimizeClicked: appDelegate.minimize()
630  onDecorationPressed: { appDelegate.focus = true; }
631  }
632 
633  WindowedFullscreenPolicy {
634  id: fullscreenPolicy
635  active: true
636  surface: model.surface
637  }
638  }
639  }
640  }
641 
642  EdgeBarrier {
643  id: edgeBarrier
644 
645  // NB: it does its own positioning according to the specified edge
646  edge: Qt.RightEdge
647 
648  onPassed: { spread.show(); }
649  material: Component {
650  Item {
651  Rectangle {
652  width: parent.height
653  height: parent.width
654  rotation: 90
655  anchors.centerIn: parent
656  gradient: Gradient {
657  GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.5)}
658  GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}
659  }
660  }
661  }
662  }
663  }
664 
665  SwipeArea {
666  direction: Direction.Leftwards
667  anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
668  width: units.gu(1)
669  onDraggingChanged: { if (dragging) { spread.show(); } }
670  }
671 
672  DesktopSpread {
673  id: spread
674  objectName: "spread"
675  anchors.fill: appContainer
676  workspace: appContainer
677  focus: state == "altTab"
678  altTabPressed: root.altTabPressed
679 
680  onPlayFocusAnimation: {
681  appRepeater.itemAt(index).playFocusAnimation();
682  }
683  }
684 }