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 
30  // functions to be called from outside
31  function updateFocusedAppOrientation() { /* TODO */ }
32  function updateFocusedAppOrientationAnimated() { /* TODO */}
33  function pushRightEdge(amount) {
34  if (spread.state === "") {
35  edgeBarrier.push(amount);
36  }
37  }
38 
39  // Used by TutorialRight
40  property bool spreadShown: spread.state == "altTab"
41 
42  mainApp: ApplicationManager.focusedApplicationId
43  ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
44  : null
45 
46  mainAppWindow: priv.focusedAppDelegate ? priv.focusedAppDelegate.appWindow : null
47 
48  // application windows never rotate independently
49  mainAppWindowOrientationAngle: shellOrientationAngle
50 
51  orientationChangesEnabled: true
52 
53  Connections {
54  target: ApplicationManager
55  onApplicationAdded: {
56  if (spread.state == "altTab") {
57  spread.state = "";
58  }
59 
60  ApplicationManager.focusApplication(appId);
61  }
62 
63  onApplicationRemoved: {
64  priv.focusNext();
65  }
66 
67  onFocusRequested: {
68  var appIndex = priv.indexOf(appId);
69  var appDelegate = appRepeater.itemAt(appIndex);
70  appDelegate.restore();
71 
72  if (spread.state == "altTab") {
73  spread.cancel();
74  }
75  }
76  }
77 
78  GlobalShortcut {
79  id: closeWindowShortcut
80  shortcut: Qt.AltModifier|Qt.Key_F4
81  onTriggered: ApplicationManager.stopApplication(priv.focusedAppId)
82  active: priv.focusedAppId !== ""
83  }
84 
85  GlobalShortcut {
86  id: showSpreadShortcut
87  shortcut: Qt.MetaModifier|Qt.Key_W
88  onTriggered: spread.state = "altTab"
89  }
90 
91  GlobalShortcut {
92  id: minimizeAllShortcut
93  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_D
94  onTriggered: priv.minimizeAllWindows()
95  }
96 
97  GlobalShortcut {
98  id: maximizeWindowShortcut
99  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Up
100  onTriggered: priv.focusedAppDelegate.maximize()
101  active: priv.focusedAppDelegate !== null
102  }
103 
104  GlobalShortcut {
105  id: maximizeWindowLeftShortcut
106  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Left
107  onTriggered: priv.focusedAppDelegate.maximizeLeft()
108  active: priv.focusedAppDelegate !== null
109  }
110 
111  GlobalShortcut {
112  id: maximizeWindowRightShortcut
113  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Right
114  onTriggered: priv.focusedAppDelegate.maximizeRight()
115  active: priv.focusedAppDelegate !== null
116  }
117 
118  GlobalShortcut {
119  id: minimizeRestoreShortcut
120  shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Down
121  onTriggered: priv.focusedAppDelegate.maximized || priv.focusedAppDelegate.maximizedLeft || priv.focusedAppDelegate.maximizedRight
122  ? priv.focusedAppDelegate.restoreFromMaximized() : priv.focusedAppDelegate.minimize()
123  active: priv.focusedAppDelegate !== null
124  }
125 
126  QtObject {
127  id: priv
128 
129  readonly property string focusedAppId: ApplicationManager.focusedApplicationId
130  readonly property var focusedAppDelegate: {
131  var index = indexOf(focusedAppId);
132  return index >= 0 && index < appRepeater.count ? appRepeater.itemAt(index) : null
133  }
134  onFocusedAppDelegateChanged: updateForegroundMaximizedApp();
135 
136  property int foregroundMaximizedAppZ: -1
137  property int foregroundMaximizedAppIndex: -1 // for stuff like drop shadow and focusing maximized app by clicking panel
138 
139  function updateForegroundMaximizedApp() {
140  var tmp = -1;
141  var tmpAppId = -1;
142  for (var i = appRepeater.count - 1; i >= 0; i--) {
143  var item = appRepeater.itemAt(i);
144  if (item && item.visuallyMaximized) {
145  tmpAppId = i;
146  tmp = Math.max(tmp, item.normalZ);
147  }
148  }
149  foregroundMaximizedAppZ = tmp;
150  foregroundMaximizedAppIndex = tmpAppId;
151  }
152 
153  function indexOf(appId) {
154  for (var i = 0; i < ApplicationManager.count; i++) {
155  if (ApplicationManager.get(i).appId == appId) {
156  return i;
157  }
158  }
159  return -1;
160  }
161 
162  function minimizeAllWindows() {
163  for (var i = 0; i < appRepeater.count; i++) {
164  var appDelegate = appRepeater.itemAt(i);
165  if (appDelegate && !appDelegate.minimized) {
166  appDelegate.minimize();
167  }
168  }
169 
170  ApplicationManager.unfocusCurrentApplication(); // no app should have focus at this point
171  }
172 
173  function focusNext() {
174  ApplicationManager.unfocusCurrentApplication();
175  for (var i = 0; i < appRepeater.count; i++) {
176  var appDelegate = appRepeater.itemAt(i);
177  if (appDelegate && !appDelegate.minimized) {
178  ApplicationManager.focusApplication(appDelegate.appId);
179  return;
180  }
181  }
182  }
183  }
184 
185  Connections {
186  target: PanelState
187  onClose: {
188  ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
189  }
190  onMinimize: priv.focusedAppDelegate && priv.focusedAppDelegate.minimize();
191  onMaximize: priv.focusedAppDelegate // don't restore minimized apps when double clicking the panel
192  && priv.focusedAppDelegate.restoreFromMaximized();
193  onFocusMaximizedApp: if (priv.foregroundMaximizedAppIndex != -1) {
194  ApplicationManager.focusApplication(appRepeater.itemAt(priv.foregroundMaximizedAppIndex).appId);
195  }
196  }
197 
198  Binding {
199  target: PanelState
200  property: "buttonsVisible"
201  value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized // FIXME for Locally integrated menus
202  && spread.state == ""
203  }
204 
205  Binding {
206  target: PanelState
207  property: "title"
208  value: {
209  if (priv.focusedAppDelegate !== null && spread.state == "") {
210  if (priv.focusedAppDelegate.maximized)
211  return priv.focusedAppDelegate.title
212  else
213  return priv.focusedAppDelegate.appName
214  }
215  return ""
216  }
217  when: priv.focusedAppDelegate
218  }
219 
220  Binding {
221  target: PanelState
222  property: "dropShadow"
223  value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppIndex !== -1
224  }
225 
226  Component.onDestruction: {
227  PanelState.title = "";
228  PanelState.buttonsVisible = false;
229  PanelState.dropShadow = false;
230  }
231 
232 
233  FocusScope {
234  id: appContainer
235  objectName: "appContainer"
236  anchors.fill: parent
237  focus: spread.state !== "altTab"
238 
239  CrossFadeImage {
240  id: wallpaper
241  anchors.fill: parent
242  source: root.background
243  sourceSize { height: root.height; width: root.width }
244  fillMode: Image.PreserveAspectCrop
245  }
246 
247  Repeater {
248  id: appRepeater
249  model: ApplicationManager
250  objectName: "appRepeater"
251 
252  delegate: FocusScope {
253  id: appDelegate
254  objectName: "appDelegate_" + appId
255  // z might be overriden in some cases by effects, but we need z ordering
256  // to calculate occlusion detection
257  property int normalZ: ApplicationManager.count - index
258  z: normalZ
259  y: PanelState.panelHeight
260  focus: appId === priv.focusedAppId
261  width: decoratedWindow.width
262  height: decoratedWindow.height
263  property int requestedWidth: -1
264  property int requestedHeight: -1
265  property alias minimumWidth: decoratedWindow.minimumWidth
266  property alias minimumHeight: decoratedWindow.minimumHeight
267  property alias maximumWidth: decoratedWindow.maximumWidth
268  property alias maximumHeight: decoratedWindow.maximumHeight
269  property alias widthIncrement: decoratedWindow.widthIncrement
270  property alias heightIncrement: decoratedWindow.heightIncrement
271 
272  QtObject {
273  id: appDelegatePrivate
274  property bool maximized: false
275  property bool maximizedLeft: false
276  property bool maximizedRight: false
277  property bool minimized: false
278  }
279  readonly property alias maximized: appDelegatePrivate.maximized
280  readonly property alias maximizedLeft: appDelegatePrivate.maximizedLeft
281  readonly property alias maximizedRight: appDelegatePrivate.maximizedRight
282  readonly property alias minimized: appDelegatePrivate.minimized
283  readonly property alias fullscreen: decoratedWindow.fullscreen
284 
285  readonly property string appId: model.appId
286  property bool animationsEnabled: true
287  property alias title: decoratedWindow.title
288  readonly property string appName: model.name
289  property bool visuallyMaximized: false
290  property bool visuallyMinimized: false
291 
292  readonly property alias appWindow: decoratedWindow.window
293 
294  onFocusChanged: {
295  if (focus && ApplicationManager.focusedApplicationId !== appId) {
296  ApplicationManager.focusApplication(appId);
297  }
298  }
299 
300  onVisuallyMaximizedChanged: priv.updateForegroundMaximizedApp()
301 
302  visible: !visuallyMinimized &&
303  !greeter.fullyShown &&
304  (priv.foregroundMaximizedAppZ === -1 || priv.foregroundMaximizedAppZ <= z) ||
305  decoratedWindow.fullscreen ||
306  (spread.state == "altTab" && index === spread.highlightedIndex)
307 
308  Binding {
309  target: ApplicationManager.get(index)
310  property: "requestedState"
311  // TODO: figure out some lifecycle policy, like suspending minimized apps
312  // if running on a tablet or something.
313  // TODO: If the device has a dozen suspended apps because it was running
314  // in staged mode, when it switches to Windowed mode it will suddenly
315  // resume all those apps at once. We might want to avoid that.
316  value: ApplicationInfoInterface.RequestedRunning // Always running for now
317  }
318 
319  function maximize(animated) {
320  animationsEnabled = (animated === undefined) || animated;
321  appDelegatePrivate.minimized = false;
322  appDelegatePrivate.maximized = true;
323  appDelegatePrivate.maximizedLeft = false;
324  appDelegatePrivate.maximizedRight = false;
325  }
326  function maximizeLeft() {
327  appDelegatePrivate.minimized = false;
328  appDelegatePrivate.maximized = false;
329  appDelegatePrivate.maximizedLeft = true;
330  appDelegatePrivate.maximizedRight = false;
331  }
332  function maximizeRight() {
333  appDelegatePrivate.minimized = false;
334  appDelegatePrivate.maximized = false;
335  appDelegatePrivate.maximizedLeft = false;
336  appDelegatePrivate.maximizedRight = true;
337  }
338  function minimize(animated) {
339  animationsEnabled = (animated === undefined) || animated;
340  appDelegatePrivate.minimized = true;
341  }
342  function restoreFromMaximized(animated) {
343  animationsEnabled = (animated === undefined) || animated;
344  appDelegatePrivate.minimized = false;
345  appDelegatePrivate.maximized = false;
346  appDelegatePrivate.maximizedLeft = false;
347  appDelegatePrivate.maximizedRight = false;
348  }
349  function restore(animated) {
350  animationsEnabled = (animated === undefined) || animated;
351  appDelegatePrivate.minimized = false;
352  if (maximized)
353  maximize();
354  else if (maximizedLeft)
355  maximizeLeft();
356  else if (maximizedRight)
357  maximizeRight();
358  ApplicationManager.focusApplication(appId);
359  }
360 
361  function playFocusAnimation() {
362  focusAnimation.start()
363  }
364 
365  UbuntuNumberAnimation {
366  id: focusAnimation
367  target: appDelegate
368  property: "scale"
369  from: 0.98
370  to: 1
371  duration: UbuntuAnimation.SnapDuration
372  }
373 
374  states: [
375  State {
376  name: "fullscreen"; when: decoratedWindow.fullscreen
377  PropertyChanges {
378  target: appDelegate;
379  x: 0;
380  y: -PanelState.panelHeight
381  requestedWidth: appContainer.width;
382  requestedHeight: appContainer.height;
383  }
384  },
385  State {
386  name: "normal";
387  when: !appDelegate.maximized && !appDelegate.minimized
388  && !appDelegate.maximizedLeft && !appDelegate.maximizedRight
389  PropertyChanges {
390  target: appDelegate;
391  visuallyMinimized: false;
392  visuallyMaximized: false
393  }
394  },
395  State {
396  name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
397  PropertyChanges {
398  target: appDelegate;
399  x: root.leftMargin;
400  y: 0;
401  visuallyMinimized: false;
402  visuallyMaximized: true
403  }
404  PropertyChanges {
405  target: decoratedWindow
406  requestedWidth: appContainer.width - root.leftMargin;
407  requestedHeight: appContainer.height;
408  }
409  },
410  State {
411  name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
412  PropertyChanges {
413  target: appDelegate
414  x: root.leftMargin
415  y: PanelState.panelHeight
416  }
417  PropertyChanges {
418  target: decoratedWindow
419  requestedWidth: (appContainer.width - root.leftMargin)/2
420  requestedHeight: appContainer.height - PanelState.panelHeight
421  }
422  },
423  State {
424  name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
425  PropertyChanges {
426  target: appDelegate;
427  x: (appContainer.width + root.leftMargin)/2
428  y: PanelState.panelHeight
429  }
430  PropertyChanges {
431  target: decoratedWindow
432  requestedWidth: (appContainer.width - root.leftMargin)/2
433  requestedHeight: appContainer.height - PanelState.panelHeight
434  }
435  },
436  State {
437  name: "minimized"; when: appDelegate.minimized
438  PropertyChanges {
439  target: appDelegate;
440  x: -appDelegate.width / 2;
441  scale: units.gu(5) / appDelegate.width;
442  opacity: 0
443  visuallyMinimized: true;
444  visuallyMaximized: false
445  }
446  }
447  ]
448  transitions: [
449  Transition {
450  to: "normal"
451  enabled: appDelegate.animationsEnabled
452  PropertyAction { target: appDelegate; properties: "visuallyMinimized,visuallyMaximized" }
453  UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
454  UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
455  },
456  Transition {
457  to: "minimized"
458  enabled: appDelegate.animationsEnabled
459  PropertyAction { target: appDelegate; property: "visuallyMaximized" }
460  SequentialAnimation {
461  ParallelAnimation {
462  UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
463  UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
464  }
465  PropertyAction { target: appDelegate; property: "visuallyMinimized" }
466  ScriptAction {
467  script: {
468  if (appDelegate.minimized) {
469  priv.focusNext();
470  }
471  }
472  }
473  }
474  },
475  Transition {
476  to: "*" //maximized and fullscreen
477  enabled: appDelegate.animationsEnabled
478  PropertyAction { target: appDelegate; property: "visuallyMinimized" }
479  SequentialAnimation {
480  ParallelAnimation {
481  UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
482  UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
483  }
484  PropertyAction { target: appDelegate; property: "visuallyMaximized" }
485  }
486  }
487  ]
488 
489  Binding {
490  id: previewBinding
491  target: appDelegate
492  property: "z"
493  value: ApplicationManager.count + 1
494  when: index == spread.highlightedIndex && spread.ready
495  }
496 
497  WindowResizeArea {
498  id: resizeArea
499  objectName: "windowResizeArea"
500  target: appDelegate
501  minWidth: units.gu(10)
502  minHeight: units.gu(10)
503  borderThickness: units.gu(2)
504  windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
505  screenWidth: appContainer.width
506  screenHeight: appContainer.height
507  leftMargin: root.leftMargin
508 
509  onPressed: { ApplicationManager.focusApplication(model.appId) }
510 
511  Component.onCompleted: {
512  loadWindowState();
513  }
514 
515  property bool saveStateOnDestruction: true
516  Connections {
517  target: root
518  onStageAboutToBeUnloaded: {
519  resizeArea.saveWindowState();
520  resizeArea.saveStateOnDestruction = false;
521  fullscreenPolicy.active = false;
522  }
523  }
524  Component.onDestruction: {
525  if (saveStateOnDestruction) {
526  saveWindowState();
527  }
528  }
529  }
530 
531  DecoratedWindow {
532  id: decoratedWindow
533  objectName: "decoratedWindow"
534  anchors.left: appDelegate.left
535  anchors.top: appDelegate.top
536  application: ApplicationManager.get(index)
537  active: ApplicationManager.focusedApplicationId === model.appId
538  focus: true
539 
540  requestedWidth: appDelegate.requestedWidth
541  requestedHeight: appDelegate.requestedHeight
542 
543  onClose: ApplicationManager.stopApplication(model.appId)
544  onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
545  ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
546  onMinimize: appDelegate.minimize()
547  onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
548  }
549 
550  WindowedFullscreenPolicy {
551  id: fullscreenPolicy
552  active: true
553  application: decoratedWindow.application
554  }
555  }
556  }
557  }
558 
559  EdgeBarrier {
560  id: edgeBarrier
561 
562  // NB: it does its own positioning according to the specified edge
563  edge: Qt.RightEdge
564 
565  onPassed: { spread.show(); }
566  material: Component {
567  Item {
568  Rectangle {
569  width: parent.height
570  height: parent.width
571  rotation: 90
572  anchors.centerIn: parent
573  gradient: Gradient {
574  GradientStop { position: 0.0; color: Qt.rgba(0.16,0.16,0.16,0.5)}
575  GradientStop { position: 1.0; color: Qt.rgba(0.16,0.16,0.16,0)}
576  }
577  }
578  }
579  }
580  }
581 
582  DirectionalDragArea {
583  direction: Direction.Leftwards
584  anchors { top: parent.top; right: parent.right; bottom: parent.bottom }
585  width: units.gu(1)
586  onDraggingChanged: { if (dragging) { spread.show(); } }
587  }
588 
589  DesktopSpread {
590  id: spread
591  objectName: "spread"
592  anchors.fill: appContainer
593  workspace: appContainer
594  focus: state == "altTab"
595  altTabPressed: root.altTabPressed
596 
597  onPlayFocusAnimation: {
598  appRepeater.itemAt(index).playFocusAnimation();
599  }
600  }
601 }