on xenial help sign in
Fix splashscreen orientation when app rotates own contents

+ Refactor states code in SpreadDelegate, removing redundancy + Fix mock MirSurfaceItem::orientationAngle implementation, mirroring what the real implementation in qtmir does. (LP: #1586050) Approved by: Albert Astals Cid

commit9ca0c7906f9ce8f1b7d5d57249d7b4681b3b7ecd authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateWed 3 Aug 2016, 15:48 (10 years ago) parent1abf9d5ca86d0a
5 files changed+231−103
qml/Stages/ApplicationWindow.qml+6 qml/Stages/SpreadDelegate.qml+71−81 tests/mocks/Unity/Application/MirSurfaceItem.cpp+37−10 tests/mocks/Unity/Application/MirSurfaceItem.h+2 tests/qmltests/Stages/tst_SpreadDelegate.qml+115−12
qml/Stages/ApplicationWindow.qml+6
@@ -46,6 +46,7 @@ FocusScope {
4646 property alias resizeSurface: surfaceContainer.resizeSurface
4747 property int requestedWidth: -1
4848 property int requestedHeight: -1
49+ property real splashRotation: 0
4950
5051 readonly property int minimumWidth: surface ? surface.minimumWidth : 0
5152 readonly property int minimumHeight: surface ? surface.minimumHeight : 0
@@ -184,6 +185,11 @@ FocusScope {
184185 backgroundColor: d.splashColor
185186 headerColor: d.splashColorHeader
186187 footerColor: d.splashColorFooter
188+
189+ rotation: root.splashRotation
190+ anchors.centerIn: parent
191+ width: rotation == 0 || rotation == 180 ? root.width : root.height
192+ height: rotation == 0 || rotation == 180 ? root.height : root.width
187193 }
188194 }
189195 }
qml/Stages/SpreadDelegate.qml+71−81
@@ -82,12 +82,69 @@ FocusScope {
8282
8383 QtObject {
8484 id: priv
85+ objectName: "spreadDelegatePriv"
8586 property bool startingUp: true
8687 }
8788
8889 Component.onCompleted: { finishStartUpTimer.start(); }
8990 Timer { id: finishStartUpTimer; interval: 400; onTriggered: priv.startingUp = false }
9091
92+ // JS version of QPlatformScreen::angleBetween C++ implementation.
93+ // So don't ask me how it works because I don't know.
94+ // Calling Screen.angleBetween from within a Binding component doesn't work for some reason.
95+ function angleBetween(a, b) {
96+ if (a == b)
97+ return 0;
98+
99+ var ia = Math.log(a) / Math.LN2;
100+ var ib = Math.log(b) / Math.LN2;
101+
102+ var delta = ia - ib;
103+
104+ if (delta < 0)
105+ delta = delta + 4;
106+
107+ var angles = [ 0, 90, 180, 270 ];
108+ return angles[delta];
109+ }
110+
111+ // Sets the initial orientationAngle of the window, when it first slides into view
112+ // (with the splash screen likely being displayed). At that point we just try to
113+ // match shell's current orientation. We need a bit of time in this state as the
114+ // information we need to decide orientationAngle may take a few cycles to
115+ // be set.
116+ Binding {
117+ target: appWindowWithShadow
118+ property: "orientationAngle"
119+ when: priv.startingUp
120+ value: {
121+ var supportedOrientations = root.supportedOrientations;
122+
123+ if (supportedOrientations === Qt.PrimaryOrientation) {
124+ supportedOrientations = root.orientations.primary;
125+ }
126+
127+ // If it doesn't support shell's current orientation
128+ // then simply pick some arbitraty one that it does support
129+ var chosenOrientation = 0;
130+ if (supportedOrientations & root.shellOrientation) {
131+ chosenOrientation = root.shellOrientation;
132+ } else if (supportedOrientations & Qt.PortraitOrientation) {
133+ chosenOrientation = root.orientations.portrait;
134+ } else if (supportedOrientations & Qt.LandscapeOrientation) {
135+ chosenOrientation = root.orientations.landscape;
136+ } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
137+ chosenOrientation = root.orientations.invertedPortrait;
138+ } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
139+ chosenOrientation = root.orientations.invertedLandscape;
140+ } else {
141+ chosenOrientation = root.orientations.primary;
142+ }
143+
144+ return angleBetween(root.orientations.native_, chosenOrientation);
145+ }
146+ }
147+
91148 Rectangle {
92149 id: background
93150 color: "black"
@@ -129,9 +186,7 @@ FocusScope {
129186 }
130187
131188 state: {
132− if (priv.startingUp) {
133− return "startingUp";
134− } else if (root.application && root.application.rotatesWindowContents) {
189+ if (root.application && root.application.rotatesWindowContents) {
135190 return "counterRotate";
136191 } else if (orientationChangeAnimation.running) {
137192 return "animatingRotation";
@@ -149,118 +204,53 @@ FocusScope {
149204 }
150205
151206 states: [
152− // Sets the initial orientationAngle of the window, when it first slides into view
153− // (with the splash screen likely being displayed). At that point we just try to
154− // match shell's current orientation. We need a bit of time in this state as the
155− // information we need to decide orientationAngle may take a few cycles to
156− // be set.
207+ // A base state containing bindings common to others
208+ // Ensures appWindowWithShadow fills the root area.
157209 State {
158− name: "startingUp"
210+ name: "fillRootArea"
159211 PropertyChanges {
160212 target: appWindowWithShadow
161213 restoreEntryValues: false
162− orientationAngle: {
163− if (!root.application || root.application.rotatesWindowContents) {
164− return 0;
165− }
166−
167− var supportedOrientations = root.supportedOrientations;
168−
169− if (supportedOrientations === Qt.PrimaryOrientation) {
170− supportedOrientations = root.orientations.primary;
171− }
172−
173− // If it doesn't support shell's current orientation
174− // then simply pick some arbitraty one that it does support
175− var chosenOrientation = 0;
176− if (supportedOrientations & root.shellOrientation) {
177− chosenOrientation = root.shellOrientation;
178− } else if (supportedOrientations & Qt.PortraitOrientation) {
179− chosenOrientation = root.orientations.portrait;
180− } else if (supportedOrientations & Qt.LandscapeOrientation) {
181− chosenOrientation = root.orientations.landscape;
182− } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
183− chosenOrientation = root.orientations.invertedPortrait;
184− } else if (supportedOrientations & Qt.InvertedLandscapeOrientation) {
185− chosenOrientation = root.orientations.invertedLandscape;
186− } else {
187− chosenOrientation = root.orientations.primary;
188− }
189−
190− return Screen.angleBetween(root.orientations.native_, chosenOrientation);
191− }
192−
193− rotation: normalizeAngle(appWindowWithShadow.orientationAngle - root.shellOrientationAngle)
194− width: {
195− if (rotation == 0 || rotation == 180) {
196− return root.width;
197− } else {
198− return root.height;
199− }
200− }
201− height: {
202− if (rotation == 0 || rotation == 180)
203− return root.height;
204− else
205− return root.width;
206− }
214+ width: appWindowWithShadow.rotation == 0 || appWindowWithShadow.rotation == 180 ? root.width : root.height
215+ height: appWindowWithShadow.rotation == 0 || appWindowWithShadow.rotation == 180 ? root.height : root.width
207216 }
208217 },
209218 // In this state we stick to our currently set orientationAngle, which may change only due
210219 // to calls made to matchShellOrientation() or animateToShellOrientation()
211220 State {
212− id: keepSceneRotationState
213221 name: "keepSceneRotation"
214−
215− StateChangeScript { script: {
216− // break binding
217− appWindowWithShadow.orientationAngle = appWindowWithShadow.orientationAngle;
218− } }
222+ extend: "fillRootArea"
219223 PropertyChanges {
220224 target: appWindowWithShadow
221225 restoreEntryValues: false
222226 rotation: normalizeAngle(appWindowWithShadow.orientationAngle - root.shellOrientationAngle)
223− width: {
224− if (rotation == 0 || rotation == 180) {
225− return root.width;
226− } else {
227− return root.height;
228− }
229− }
230− height: {
231− if (rotation == 0 || rotation == 180)
232− return root.height;
233− else
234− return root.width;
235− }
236227 }
237228 },
238229 // In this state we counteract any shell rotation so that the window, in scene coordinates,
239230 // remains unrotated.
231+ // But the splash screen should still obey the set orientationAngle set by shell
240232 State {
241233 name: "counterRotate"
242− StateChangeScript { script: {
243− // break binding
244− appWindowWithShadow.orientationAngle = appWindowWithShadow.orientationAngle;
245− } }
234+ extend: "fillRootArea"
246235 PropertyChanges {
247236 target: appWindowWithShadow
248− width: root.shellOrientationAngle == 0 || root.shellOrientationAngle == 180 ? root.width : root.height
249− height: root.shellOrientationAngle == 0 || root.shellOrientationAngle == 180 ? root.height : root.width
250237 rotation: normalizeAngle(-root.shellOrientationAngle)
251238 }
252239 PropertyChanges {
253240 target: appWindow
254241 surfaceOrientationAngle: appWindowWithShadow.orientationAngle
242+ splashRotation: appWindowWithShadow.orientationAngle
255243 }
256244 },
245+ // Dummy state.
246+ // The animation may indiscriminately break any bindings assigned to appWindowWithShadow rotation,
247+ // width or height.
257248 State {
258249 name: "animatingRotation"
259250 }
260251 ]
261252
262− x: (parent.width - width) / 2
263− y: (parent.height - height) / 2
253+ anchors.centerIn: parent
264254
265255 BorderImage {
266256 anchors {
tests/mocks/Unity/Application/MirSurfaceItem.cpp+37−10
@@ -43,6 +43,7 @@ MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
4343 , m_qmlContentComponent(nullptr)
4444 , m_qmlItem(nullptr)
4545 , m_consumesInput(false)
46+ , m_orientationAngle(nullptr)
4647 , m_surfaceWidth(0)
4748 , m_surfaceHeight(0)
4849 , m_touchPressCount(0)
@@ -68,6 +69,7 @@ MirSurfaceItem::~MirSurfaceItem()
6869{
6970 DEBUG_MSG((void*)(this) << name());
7071 setSurface(nullptr);
72+ delete m_orientationAngle;
7173}
7274
7375void MirSurfaceItem::printComponentErrors()
@@ -125,7 +127,10 @@ Mir::ShellChrome MirSurfaceItem::shellChrome() const
125127
126128Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
127129{
128− if (m_qmlSurface) {
130+ if (m_orientationAngle) {
131+ Q_ASSERT(!m_qmlSurface);
132+ return *m_orientationAngle;
133+ } else if (m_qmlSurface) {
129134 return m_qmlSurface->orientationAngle();
130135 } else {
131136 return Mir::Angle0;
@@ -134,16 +139,24 @@ Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
134139
135140void MirSurfaceItem::setOrientationAngle(Mir::OrientationAngle angle)
136141{
137− if (!m_qmlSurface)
138− return;
139−
140− if (m_qmlSurface->orientationAngle() == angle)
141− return;
142+ DEBUG_MSG(angle);
142143
143− m_qmlSurface->setOrientationAngle(angle);
144+ if (m_qmlSurface) {
145+ Q_ASSERT(!m_orientationAngle);
146+ m_qmlSurface->setOrientationAngle(angle);
147+ } else if (!m_orientationAngle) {
148+ m_orientationAngle = new Mir::OrientationAngle;
149+ *m_orientationAngle = angle;
150+ Q_EMIT orientationAngleChanged(angle);
151+ } else if (*m_orientationAngle != angle) {
152+ *m_orientationAngle = angle;
153+ Q_EMIT orientationAngleChanged(angle);
154+ }
144155
145− QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
146− orientationProp.write(QVariant::fromValue(m_qmlSurface->orientationAngle()));
156+ if (m_qmlItem) {
157+ QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
158+ orientationProp.write(QVariant::fromValue(orientationAngle()));
159+ }
147160}
148161
149162void MirSurfaceItem::updateScreenshot(QUrl screenshotUrl)
@@ -175,6 +188,11 @@ void MirSurfaceItem::createQmlContentItem()
175188 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
176189 screenshotSource.write(QVariant::fromValue(m_qmlSurface->screenshotUrl()));
177190 }
191+
192+ {
193+ QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
194+ orientationProp.write(QVariant::fromValue(orientationAngle()));
195+ }
178196}
179197
180198void MirSurfaceItem::touchEvent(QTouchEvent * event)
@@ -255,7 +273,16 @@ void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
255273 updateSurfaceSize();
256274 updateMirSurfaceVisibility();
257275
258− connect(m_qmlSurface, &MirSurface::orientationAngleChanged, this, &MirSurfaceItem::orientationAngleChanged);
276+ if (m_orientationAngle) {
277+ m_qmlSurface->setOrientationAngle(*m_orientationAngle);
278+ connect(m_qmlSurface, &MirSurfaceInterface::orientationAngleChanged, this, &MirSurfaceItem::orientationAngleChanged);
279+ delete m_orientationAngle;
280+ m_orientationAngle = nullptr;
281+ } else {
282+ connect(m_qmlSurface, &MirSurfaceInterface::orientationAngleChanged, this, &MirSurfaceItem::orientationAngleChanged);
283+ Q_EMIT orientationAngleChanged(m_qmlSurface->orientationAngle());
284+ }
285+
259286 connect(m_qmlSurface, &MirSurface::screenshotUrlChanged, this, &MirSurfaceItem::updateScreenshot);
260287 connect(m_qmlSurface, &MirSurface::liveChanged, this, &MirSurfaceItem::liveChanged);
261288 connect(m_qmlSurface, &MirSurface::stateChanged, this, &MirSurfaceItem::surfaceStateChanged);
tests/mocks/Unity/Application/MirSurfaceItem.h+2
@@ -117,6 +117,8 @@ private:
117117
118118 bool m_consumesInput;
119119
120+ Mir::OrientationAngle *m_orientationAngle;
121+
120122 int m_surfaceWidth;
121123 int m_surfaceHeight;
122124
tests/qmltests/Stages/tst_SpreadDelegate.qml+115−12
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -132,10 +132,12 @@ Rectangle {
132132 property int value: selectedIndex * 90
133133 }
134134 Button {
135+ id: matchShellButton
135136 text: "matchShellOrientation()"
136137 onClicked: { spreadDelegateLoader.item.matchShellOrientation(); }
137138 }
138139 Button {
140+ id: animateToShellButton
139141 text: "animateToShellOrientation()"
140142 onClicked: { spreadDelegateLoader.item.animateToShellOrientation(); }
141143 }
@@ -197,6 +199,77 @@ Rectangle {
197199
198200 }
199201
202+ function waitUntilAppWindowIsFullyLoaded() {
203+ var appWindowStateGroup = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
204+ tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
205+ waitUntilTransitionsEnd(appWindowStateGroup);
206+
207+ var priv = findInvisibleChild(spreadDelegate, "spreadDelegatePriv");
208+ verify(priv);
209+ tryCompare(priv, "startingUp", false);
210+ }
211+
212+ function rotateShellTo(angle) {
213+ shellOrientationAngleSelector.selectedIndex = angle / 90;
214+ }
215+
216+ function waitUntilRotationAnimationStops() {
217+ var orientationChangeAnimation = findInvisibleChild(spreadDelegate, "orientationChangeAnimation");
218+ verify(orientationChangeAnimation);
219+ tryCompare(orientationChangeAnimation, "running", false);
220+ }
221+
222+ function checkAppWindowTransformationMatchesRotation(appWindow, angle) {
223+
224+ var topLeftX = 0;
225+ var topLeftY = 0;
226+ var bottomRightX = 0;
227+ var bottomRightY = 0;
228+
229+ switch (angle) {
230+ case 0:
231+ topLeftX = 0;
232+ topLeftY = 0;
233+ bottomRightX = fakeShell.shortestDimension;
234+ bottomRightY = fakeShell.longestDimension;
235+ break;
236+ case 90:
237+ topLeftX = fakeShell.shortestDimension;
238+ topLeftY = 0;
239+ bottomRightX = 0;
240+ bottomRightY = fakeShell.longestDimension;
241+ break;
242+ default:
243+ // TODO implement 180 and 270 once we need them
244+ verify(false);
245+ }
246+
247+ var appWindowTopLeftInRootCoords = appWindow.mapToItem(root, 0, 0);
248+
249+ if (appWindowTopLeftInRootCoords.x !== topLeftX) {
250+ qtest_fail("appWindow topLeft.x ("+appWindowTopLeftInRootCoords.x+")"
251+ +" doesn't match expectations ("+topLeftX+").", 1);
252+ }
253+ if (appWindowTopLeftInRootCoords.y !== topLeftY) {
254+ qtest_fail("appWindow topLeft.y ("+appWindowTopLeftInRootCoords.y+")"
255+ +" doesn't match expectations ("+topLeftY+").", 1);
256+ }
257+
258+ var appWindowBottomRightInRootCoords = appWindow.mapToItem(root, appWindow.width,
259+ appWindow.height);
260+ compare(appWindowBottomRightInRootCoords.x, bottomRightX);
261+ compare(appWindowBottomRightInRootCoords.y, bottomRightY);
262+
263+ if (appWindowBottomRightInRootCoords.x !== bottomRightX) {
264+ qtest_fail("appWindow bottomRight.x ("+appWindowBottomRightInRootCoords.x+")"
265+ +" doesn't match expectations ("+bottomRightX+").", 1);
266+ }
267+ if (appWindowBottomRightInRootCoords.y !== bottomRightY) {
268+ qtest_fail("appWindow bottomRight.y ("+appWindowBottomRightInRootCoords.y+")"
269+ +" doesn't match expectations ("+bottomRightY+").", 1);
270+ }
271+ }
272+
200273 function test_swipeToClose_data() {
201274 return [
202275 {tag: "swipeToClose=true closeable=true -> appWindow moves away",
@@ -278,12 +351,12 @@ Rectangle {
278351 function test_keepsSceneTransformationWhenShellRotates(data) {
279352 loadWithGalleryApp.clicked();
280353
354+ waitUntilAppWindowIsFullyLoaded();
355+
281356 var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
282357 verify(appWindowWithShadow);
283358
284− // Wait until it reaches the state we are interested on.
285− // It begins with "startingUp"
286− tryCompare(appWindowWithShadow, "state", "keepSceneRotation");
359+ compare(appWindowWithShadow.state, "keepSceneRotation");
287360
288361 shellOrientationAngleSelector.selectedIndex = data.selectedIndex;
289362
@@ -293,14 +366,7 @@ Rectangle {
293366
294367
295368 // and scene transformation must be the identity (ie, no rotation or translation)
296− var pointInDelegateCoords = appWindowWithShadow.mapToItem(root, 0, 0);
297− compare(pointInDelegateCoords.x, 0);
298− compare(pointInDelegateCoords.y, 0);
299−
300− pointInDelegateCoords = appWindowWithShadow.mapToItem(root,
301− fakeShell.shortestDimension, fakeShell.longestDimension);
302− compare(pointInDelegateCoords.x, fakeShell.shortestDimension);
303− compare(pointInDelegateCoords.y, fakeShell.longestDimension);
369+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 0);
304370 }
305371
306372 function waitForCloseAnimationToFinish() {
@@ -316,5 +382,42 @@ Rectangle {
316382 spreadDelegateLoader.item.highlightShown = true;
317383 tryCompare(highlightRect, "visible", true)
318384 }
385+
386+ /*
387+ Checks that the ApplicationWindow position, size and rotation is correct after
388+ a sequence of shell rotations folowed by rotations (immediate or not) to match them
389+ */
390+ function test_animateToShellThenMatchShell() {
391+ loadWithGalleryApp.clicked();
392+ waitUntilAppWindowIsFullyLoaded();
393+
394+ var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
395+ verify(appWindowWithShadow);
396+
397+ // appWindow transformation is 0 relative to display
398+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 0);
399+
400+ rotateShellTo(90);
401+
402+ // appWindow transformation remains 0 relative to display
403+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 0);
404+
405+ animateToShellButton.clicked();
406+ waitUntilRotationAnimationStops();
407+
408+ // appWindow transformation is now 90 relative to display, just like shell
409+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 90);
410+
411+ rotateShellTo(0);
412+
413+ // appWindow transformation is remains 90 relative to display.
414+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 90);
415+
416+ matchShellButton.clicked();
417+
418+ // appWindow transformation is back to 0 relative to display, just like shell
419+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 0);
420+
421+ }
319422 }
320423}