Unity 8
RotationStates.qml
1 /*
2  * Copyright (C) 2015,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 Powerd 0.1
20 
21 // Why the state machine is done that way:
22 // We cannot use regular PropertyChanges{} inside the State elements as steps in the
23 // transition animations must take place in a well defined order.
24 // Which means that we also cannot jump to a new state in the middle of a transition
25 // as that would make hell brake loose.
26 StateGroup {
27  id: root
28 
29  // to be set from the outside
30  property Item orientedShell
31  property Item shell
32  property Item shellCover
33  property Item shellSnapshot
34 
35  property int rotationDuration: 450
36  property int rotationEasing: Easing.InOutCubic
37  // Those values are good for debugging/development
38  //property int rotationDuration: 3000
39  //property int rotationEasing: Easing.Linear
40 
41  state: "0"
42  states: [
43  State { name: "0" },
44  State { name: "90" },
45  State { name: "180" },
46  State { name: "270" }
47  ]
48 
49  property QtObject d: QtObject {
50  id: d
51 
52  property bool startingUp: true
53  property var finishStartUpTimer: Timer {
54  interval: 500
55  onTriggered: d.startingUp = false
56  }
57  Component.onCompleted: {
58  finishStartUpTimer.start();
59  }
60 
61  property bool transitioning: false
62  onTransitioningChanged: {
63  d.tryUpdateState();
64  }
65 
66  readonly property int requestedOrientationAngle: root.orientedShell.acceptedOrientationAngle
67 
68  // Avoiding a direct call to tryUpdateState() as the state change might trigger an immediate
69  // change to Shell.orientationAngle which, in its turn, causes a reevaluation of
70  // requestedOrientationAngle (ie., OrientedShell.acceptedOrientationAngle). A reentrant evaluation
71  // of a binding is detected by QML as a binding loop and QML will deny the reevalutation, which
72  // will leave us in a bogus state.
73  //
74  // To avoid this mess we update the state in the next event loop iteration, ensuring a clean
75  // call stack.
76  onRequestedOrientationAngleChanged: {
77  stateUpdateTimer.start();
78  }
79  property Timer stateUpdateTimer: Timer {
80  id: stateUpdateTimer
81  interval: 1
82  onTriggered: { d.tryUpdateState(); }
83  }
84 
85  function tryUpdateState() {
86  if (d.transitioning || (!d.startingUp && !root.orientedShell.orientationChangesEnabled)) {
87  return;
88  }
89 
90  var requestedState = d.requestedOrientationAngle.toString();
91  if (requestedState === root.state) {
92  return;
93  }
94 
95  d.resolveAnimationType();
96 
97  var angleDiff = Math.abs(root.shell.orientationAngle - d.requestedOrientationAngle);
98  var isNinetyRotationAnimation = angleDiff == 90 || angleDiff == 270;
99  var needsShellSnapshot = d.animationType == d.fullAnimation && isNinetyRotationAnimation;
100 
101  if (needsShellSnapshot && !shellSnapshotReady) {
102  root.shellSnapshot.take();
103  // try again once we have a shell snapshot ready for use. Snapshot taking is async.
104  return;
105  }
106 
107  if (!needsShellSnapshot && shellSnapshotReady) {
108  root.shellSnapshot.discard();
109  }
110 
111  root.state = requestedState;
112  }
113 
114  property bool shellSnapshotReady: root.shellSnapshot && root.shellSnapshot.ready
115  onShellSnapshotReadyChanged: tryUpdateState();
116 
117  property Connections shellConnections: Connections {
118  target: root.orientedShell
119  onOrientationChangesEnabledChanged: {
120  d.tryUpdateState();
121  }
122  }
123 
124  property var shellBeingResized: Binding {
125  target: root.shell
126  property: "beingResized"
127  value: d.transitioning
128  }
129 
130  readonly property int fullAnimation: 0
131  readonly property int indicatorsBarAnimation: 1
132  readonly property int noAnimation: 2
133 
134  property int animationType
135 
136  // animationType update *must* take place *before* the state update.
137  // If animationType and state were updated through bindings, as with normal qml code,
138  // there would be no guarantee in the order of the binding updates, which could then
139  // cause the wrong transitions to be chosen for the state changes.
140  function resolveAnimationType() {
141  if (d.startingUp) {
142  // During start up, inital property values are still settling while we're still
143  // to render the very first frame
144  d.animationType = d.noAnimation;
145  } else if (Powerd.status === Powerd.Off) {
146  // There's no point in animating if the user can't see it (display is off).
147  d.animationType = d.noAnimation;
148  } else if (root.shell.showingGreeter) {
149  // A rotating greeter looks weird.
150  d.animationType = d.noAnimation;
151  } else {
152  if (!root.shell.mainApp) {
153  // shouldn't happen but, anyway
154  d.animationType = d.fullAnimation;
155  return;
156  }
157 
158  if (root.shell.mainApp.rotatesWindowContents) {
159  // The application will animate its own GUI, so we don't have to do anything ourselves.
160  d.animationType = d.noAnimation;
161  } else if (root.shell.mainAppWindowOrientationAngle == d.requestedOrientationAngle) {
162  // The app window is already on its final orientation angle.
163  // So we just animate the indicators bar
164  // TODO: what if the app is fullscreen?
165  d.animationType = d.indicatorsBarAnimation;
166  } else {
167  d.animationType = d.fullAnimation;
168  }
169  }
170  }
171 
172  // When an application switch takes place, d.requestedOrientationAngle and
173  // root.shell.mainAppWindowOrientationAngle get updated separately, at different moments.
174  // So, when one of those properties change, we shouldn't make a decision straight away
175  // as the other might be stale and about to be changed. So let's give it a bit of time for
176  // them to get properly updated.
177  // This approach is indeed a bit hacky.
178  property bool appWindowOrientationAngleNeedsUpdateUnstable:
179  root.shell.orientationAngle === d.requestedOrientationAngle
180  && root.shell.mainApp
181  && root.shell.mainAppWindowOrientationAngle !== root.shell.orientationAngle
182  && !d.transitioning
183  onAppWindowOrientationAngleNeedsUpdateUnstableChanged: {
184  stableTimer.restart();
185  }
186  property Timer stableTimer: Timer {
187  interval: 200
188  onTriggered: {
189  if (d.appWindowOrientationAngleNeedsUpdateUnstable) {
190  shell.updateFocusedAppOrientationAnimated();
191  }
192  }
193  }
194  }
195 
196  transitions: [
197  Transition {
198  from: "90"; to: "0"
199  enabled: d.animationType == d.fullAnimation
200  NinetyRotationAnimation { fromAngle: 90; toAngle: 0
201  info: d; shell: root.shell }
202  },
203  Transition {
204  from: "0"; to: "90"
205  enabled: d.animationType == d.fullAnimation
206  NinetyRotationAnimation { fromAngle: 0; toAngle: 90
207  info: d; shell: root.shell }
208  },
209  Transition {
210  from: "0"; to: "270"
211  enabled: d.animationType == d.fullAnimation
212  NinetyRotationAnimation { fromAngle: 0; toAngle: 270
213  info: d; shell: root.shell }
214  },
215  Transition {
216  from: "270"; to: "0"
217  enabled: d.animationType == d.fullAnimation
218  NinetyRotationAnimation { fromAngle: 270; toAngle: 0
219  info: d; shell: root.shell }
220  },
221  Transition {
222  from: "90"; to: "180"
223  enabled: d.animationType == d.fullAnimation
224  NinetyRotationAnimation { fromAngle: 90; toAngle: 180
225  info: d; shell: root.shell }
226  },
227  Transition {
228  from: "180"; to: "90"
229  enabled: d.animationType == d.fullAnimation
230  NinetyRotationAnimation { fromAngle: 180; toAngle: 90
231  info: d; shell: root.shell }
232  },
233  Transition {
234  from: "180"; to: "270"
235  enabled: d.animationType == d.fullAnimation
236  NinetyRotationAnimation { fromAngle: 180; toAngle: 270
237  info: d; shell: root.shell }
238  },
239  Transition {
240  from: "270"; to: "180"
241  enabled: d.animationType == d.fullAnimation
242  NinetyRotationAnimation { fromAngle: 270; toAngle: 180
243  info: d; shell: root.shell }
244  },
245  Transition {
246  from: "0"; to: "180"
247  enabled: d.animationType == d.fullAnimation
248  HalfLoopRotationAnimation { fromAngle: 0; toAngle: 180
249  info: d; shell: root.shell }
250  },
251  Transition {
252  from: "180"; to: "0"
253  enabled: d.animationType == d.fullAnimation
254  HalfLoopRotationAnimation { fromAngle: 180; toAngle: 0
255  info: d; shell: root.shell }
256  },
257  Transition {
258  from: "90"; to: "270"
259  enabled: d.animationType == d.fullAnimation
260  HalfLoopRotationAnimation { fromAngle: 90; toAngle: 270
261  info: d; shell: root.shell }
262  },
263  Transition {
264  from: "270"; to: "90"
265  enabled: d.animationType == d.fullAnimation
266  HalfLoopRotationAnimation { fromAngle: 270; toAngle: 90
267  info: d; shell: root.shell }
268  },
269  Transition {
270  objectName: "immediateTransition"
271  enabled: d.animationType == d.noAnimation
272  ImmediateRotationAction { info: d; shell: root.shell }
273  },
274  Transition {
275  enabled: d.animationType == d.indicatorsBarAnimation
276  SequentialAnimation {
277  ScriptAction { script: {
278  d.transitioning = true;
279  } }
280  NumberAnimation {
281  duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing
282  target: root.shell; property: "indicatorAreaShowProgress"
283  from: 1.0; to: 0.0
284  }
285  ImmediateRotationAction { info: d; shell: root.shell }
286  NumberAnimation {
287  duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing
288  target: root.shell; property: "indicatorAreaShowProgress"
289  from: 0.0; to: 1.0
290  }
291  ScriptAction { script: {
292  d.transitioning = false;
293  }}
294  }
295  }
296  ]
297 
298 }