Unity 8
RotationStates.qml
1 /*
2  * Copyright (C) 2015 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 windowScreenshot
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  d.resolveAnimationType();
93  root.state = requestedState;
94  }
95  }
96 
97  property Connections shellConnections: Connections {
98  target: root.orientedShell
99  onOrientationChangesEnabledChanged: {
100  d.tryUpdateState();
101  }
102  }
103 
104  property var shellBeingResized: Binding {
105  target: root.shell
106  property: "beingResized"
107  value: d.transitioning
108  }
109 
110  readonly property int fullAnimation: 0
111  readonly property int indicatorsBarAnimation: 1
112  readonly property int noAnimation: 2
113 
114  property int animationType
115 
116  // animationType update *must* take place *before* the state update.
117  // If animationType and state were updated through bindings, as with normal qml code,
118  // there would be no guarantee in the order of the binding updates, which could then
119  // cause the wrong transitions to be chosen for the state changes.
120  function resolveAnimationType() {
121  if (d.startingUp) {
122  // During start up, inital property values are still settling while we're still
123  // to render the very first frame
124  d.animationType = d.noAnimation;
125  } else if (Powerd.status === Powerd.Off) {
126  // There's no point in animating if the user can't see it (display is off).
127  d.animationType = d.noAnimation;
128  } else if (root.shell.showingGreeter) {
129  // A rotating greeter looks weird.
130  d.animationType = d.noAnimation;
131  } else {
132  if (!root.shell.mainApp) {
133  // shouldn't happen but, anyway
134  d.animationType = d.fullAnimation;
135  return;
136  }
137 
138  if (root.shell.mainApp.rotatesWindowContents) {
139  // The application will animate its own GUI, so we don't have to do anything ourselves.
140  d.animationType = d.noAnimation;
141  } else if (root.shell.mainAppWindowOrientationAngle == d.requestedOrientationAngle) {
142  // The app window is already on its final orientation angle.
143  // So we just animate the indicators bar
144  // TODO: what if the app is fullscreen?
145  d.animationType = d.indicatorsBarAnimation;
146  } else {
147  d.animationType = d.fullAnimation;
148  }
149  }
150  }
151 
152  // When an application switch takes place, d.requestedOrientationAngle and
153  // root.shell.mainAppWindowOrientationAngle get updated separately, at different moments.
154  // So, when one of those properties change, we shouldn't make a decision straight away
155  // as the other might be stale and about to be changed. So let's give it a bit of time for
156  // them to get properly updated.
157  // This approach is indeed a bit hacky.
158  property bool appWindowOrientationAngleNeedsUpdateUnstable:
159  root.shell.orientationAngle === d.requestedOrientationAngle
160  && root.shell.mainApp
161  && root.shell.mainAppWindowOrientationAngle !== root.shell.orientationAngle
162  && !d.transitioning
163  onAppWindowOrientationAngleNeedsUpdateUnstableChanged: {
164  stableTimer.restart();
165  }
166  property Timer stableTimer: Timer {
167  interval: 200
168  onTriggered: {
169  if (d.appWindowOrientationAngleNeedsUpdateUnstable) {
170  shell.updateFocusedAppOrientationAnimated();
171  }
172  }
173  }
174  }
175 
176  transitions: [
177  Transition {
178  from: "90"; to: "0"
179  enabled: d.animationType == d.fullAnimation
180  NinetyRotationAnimation { fromAngle: 90; toAngle: 0
181  info: d; shell: root.shell }
182  },
183  Transition {
184  from: "0"; to: "90"
185  enabled: d.animationType == d.fullAnimation
186  NinetyRotationAnimation { fromAngle: 0; toAngle: 90
187  info: d; shell: root.shell }
188  },
189  Transition {
190  from: "0"; to: "270"
191  enabled: d.animationType == d.fullAnimation
192  NinetyRotationAnimation { fromAngle: 0; toAngle: 270
193  info: d; shell: root.shell }
194  },
195  Transition {
196  from: "270"; to: "0"
197  enabled: d.animationType == d.fullAnimation
198  NinetyRotationAnimation { fromAngle: 270; toAngle: 0
199  info: d; shell: root.shell }
200  },
201  Transition {
202  from: "90"; to: "180"
203  enabled: d.animationType == d.fullAnimation
204  NinetyRotationAnimation { fromAngle: 90; toAngle: 180
205  info: d; shell: root.shell }
206  },
207  Transition {
208  from: "180"; to: "90"
209  enabled: d.animationType == d.fullAnimation
210  NinetyRotationAnimation { fromAngle: 180; toAngle: 90
211  info: d; shell: root.shell }
212  },
213  Transition {
214  from: "180"; to: "270"
215  enabled: d.animationType == d.fullAnimation
216  NinetyRotationAnimation { fromAngle: 180; toAngle: 270
217  info: d; shell: root.shell }
218  },
219  Transition {
220  from: "270"; to: "180"
221  enabled: d.animationType == d.fullAnimation
222  NinetyRotationAnimation { fromAngle: 270; toAngle: 180
223  info: d; shell: root.shell }
224  },
225  Transition {
226  from: "0"; to: "180"
227  enabled: d.animationType == d.fullAnimation
228  HalfLoopRotationAnimation { fromAngle: 0; toAngle: 180
229  info: d; shell: root.shell }
230  },
231  Transition {
232  from: "180"; to: "0"
233  enabled: d.animationType == d.fullAnimation
234  HalfLoopRotationAnimation { fromAngle: 180; toAngle: 0
235  info: d; shell: root.shell }
236  },
237  Transition {
238  from: "90"; to: "270"
239  enabled: d.animationType == d.fullAnimation
240  HalfLoopRotationAnimation { fromAngle: 90; toAngle: 270
241  info: d; shell: root.shell }
242  },
243  Transition {
244  from: "270"; to: "90"
245  enabled: d.animationType == d.fullAnimation
246  HalfLoopRotationAnimation { fromAngle: 270; toAngle: 90
247  info: d; shell: root.shell }
248  },
249  Transition {
250  objectName: "immediateTransition"
251  enabled: d.animationType == d.noAnimation
252  ImmediateRotationAction { info: d; shell: root.shell }
253  },
254  Transition {
255  enabled: d.animationType == d.indicatorsBarAnimation
256  SequentialAnimation {
257  ScriptAction { script: {
258  d.transitioning = true;
259  } }
260  NumberAnimation {
261  duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing
262  target: root.shell; property: "indicatorAreaShowProgress"
263  from: 1.0; to: 0.0
264  }
265  ImmediateRotationAction { info: d; shell: root.shell }
266  NumberAnimation {
267  duration: UbuntuAnimation.FastDuration; easing: UbuntuAnimation.StandardEasing
268  target: root.shell; property: "indicatorAreaShowProgress"
269  from: 0.0; to: 1.0
270  }
271  ScriptAction { script: {
272  d.transitioning = false;
273  }}
274  }
275  }
276  ]
277 
278 }