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