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