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