on xenial help sign in
Fix splashscreen orientation when app rotates own contents
9ca0c79Daniel d'Andrada
qml/Stages/ApplicationWindow.qml+6
@@ -46,6 +46,7 @@ FocusScope {
46 property alias resizeSurface: surfaceContainer.resizeSurface
46 property alias resizeSurface: surfaceContainer.resizeSurface
47 property int requestedWidth: -1
47 property int requestedWidth: -1
48 property int requestedHeight: -1
48 property int requestedHeight: -1
49+ property real splashRotation: 0
49
50
50 readonly property int minimumWidth: surface ? surface.minimumWidth : 0
51 readonly property int minimumWidth: surface ? surface.minimumWidth : 0
51 readonly property int minimumHeight: surface ? surface.minimumHeight : 0
52 readonly property int minimumHeight: surface ? surface.minimumHeight : 0
@@ -184,6 +185,11 @@ FocusScope {
184 backgroundColor: d.splashColor
185 backgroundColor: d.splashColor
185 headerColor: d.splashColorHeader
186 headerColor: d.splashColorHeader
186 footerColor: d.splashColorFooter
187 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
187 }
193 }
188 }
194 }
189 }
195 }
qml/Stages/SpreadDelegate.qml+71−81
@@ -82,12 +82,69 @@ FocusScope {
82
82
83 QtObject {
83 QtObject {
84 id: priv
84 id: priv
85+ objectName: "spreadDelegatePriv"
85 property bool startingUp: true
86 property bool startingUp: true
86 }
87 }
87
88
88 Component.onCompleted: { finishStartUpTimer.start(); }
89 Component.onCompleted: { finishStartUpTimer.start(); }
89 Timer { id: finishStartUpTimer; interval: 400; onTriggered: priv.startingUp = false }
90 Timer { id: finishStartUpTimer; interval: 400; onTriggered: priv.startingUp = false }
90
91
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+
91 Rectangle {
148 Rectangle {
92 id: background
149 id: background
93 color: "black"
150 color: "black"
@@ -129,9 +186,7 @@ FocusScope {
129 }
186 }
130
187
131 state: {
188 state: {
132− if (priv.startingUp) {
133− return "startingUp";
134− } else if (root.application && root.application.rotatesWindowContents) {
189+ if (root.application && root.application.rotatesWindowContents) {
135 return "counterRotate";
190 return "counterRotate";
136 } else if (orientationChangeAnimation.running) {
191 } else if (orientationChangeAnimation.running) {
137 return "animatingRotation";
192 return "animatingRotation";
@@ -149,118 +204,53 @@ FocusScope {
149 }
204 }
150
205
151 states: [
206 states: [
152− // Sets the initial orientationAngle of the window, when it first slides into view
207+ // A base state containing bindings common to others
153− // (with the splash screen likely being displayed). At that point we just try to
208+ // Ensures appWindowWithShadow fills the root area.
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.
157 State {
209 State {
158− name: "startingUp"
210+ name: "fillRootArea"
159 PropertyChanges {
211 PropertyChanges {
160 target: appWindowWithShadow
212 target: appWindowWithShadow
161 restoreEntryValues: false
213 restoreEntryValues: false
162− orientationAngle: {
214+ width: appWindowWithShadow.rotation == 0 || appWindowWithShadow.rotation == 180 ? root.width : root.height
163− if (!root.application || root.application.rotatesWindowContents) {
215+ height: appWindowWithShadow.rotation == 0 || appWindowWithShadow.rotation == 180 ? root.height : root.width
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− }
207 }
216 }
208 },
217 },
209 // In this state we stick to our currently set orientationAngle, which may change only due
218 // In this state we stick to our currently set orientationAngle, which may change only due
210 // to calls made to matchShellOrientation() or animateToShellOrientation()
219 // to calls made to matchShellOrientation() or animateToShellOrientation()
211 State {
220 State {
212− id: keepSceneRotationState
213 name: "keepSceneRotation"
221 name: "keepSceneRotation"
214−
222+ extend: "fillRootArea"
215− StateChangeScript { script: {
216− // break binding
217− appWindowWithShadow.orientationAngle = appWindowWithShadow.orientationAngle;
218− } }
219 PropertyChanges {
223 PropertyChanges {
220 target: appWindowWithShadow
224 target: appWindowWithShadow
221 restoreEntryValues: false
225 restoreEntryValues: false
222 rotation: normalizeAngle(appWindowWithShadow.orientationAngle - root.shellOrientationAngle)
226 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− }
236 }
227 }
237 },
228 },
238 // In this state we counteract any shell rotation so that the window, in scene coordinates,
229 // In this state we counteract any shell rotation so that the window, in scene coordinates,
239 // remains unrotated.
230 // remains unrotated.
231+ // But the splash screen should still obey the set orientationAngle set by shell
240 State {
232 State {
241 name: "counterRotate"
233 name: "counterRotate"
242− StateChangeScript { script: {
234+ extend: "fillRootArea"
243− // break binding
244− appWindowWithShadow.orientationAngle = appWindowWithShadow.orientationAngle;
245− } }
246 PropertyChanges {
235 PropertyChanges {
247 target: appWindowWithShadow
236 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
250 rotation: normalizeAngle(-root.shellOrientationAngle)
237 rotation: normalizeAngle(-root.shellOrientationAngle)
251 }
238 }
252 PropertyChanges {
239 PropertyChanges {
253 target: appWindow
240 target: appWindow
254 surfaceOrientationAngle: appWindowWithShadow.orientationAngle
241 surfaceOrientationAngle: appWindowWithShadow.orientationAngle
242+ splashRotation: appWindowWithShadow.orientationAngle
255 }
243 }
256 },
244 },
245+ // Dummy state.
246+ // The animation may indiscriminately break any bindings assigned to appWindowWithShadow rotation,
247+ // width or height.
257 State {
248 State {
258 name: "animatingRotation"
249 name: "animatingRotation"
259 }
250 }
260 ]
251 ]
261
252
262− x: (parent.width - width) / 2
253+ anchors.centerIn: parent
263− y: (parent.height - height) / 2
264
254
265 BorderImage {
255 BorderImage {
266 anchors {
256 anchors {
tests/mocks/Unity/Application/MirSurfaceItem.cpp+37−10
@@ -43,6 +43,7 @@ MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
43 , m_qmlContentComponent(nullptr)
43 , m_qmlContentComponent(nullptr)
44 , m_qmlItem(nullptr)
44 , m_qmlItem(nullptr)
45 , m_consumesInput(false)
45 , m_consumesInput(false)
46+ , m_orientationAngle(nullptr)
46 , m_surfaceWidth(0)
47 , m_surfaceWidth(0)
47 , m_surfaceHeight(0)
48 , m_surfaceHeight(0)
48 , m_touchPressCount(0)
49 , m_touchPressCount(0)
@@ -68,6 +69,7 @@ MirSurfaceItem::~MirSurfaceItem()
68{
69{
69 DEBUG_MSG((void*)(this) << name());
70 DEBUG_MSG((void*)(this) << name());
70 setSurface(nullptr);
71 setSurface(nullptr);
72+ delete m_orientationAngle;
71}
73}
72
74
73void MirSurfaceItem::printComponentErrors()
75void MirSurfaceItem::printComponentErrors()
@@ -125,7 +127,10 @@ Mir::ShellChrome MirSurfaceItem::shellChrome() const
125
127
126Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
128Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
127{
129{
130+ if (m_orientationAngle) {
131+ Q_ASSERT(!m_qmlSurface);
132+ return *m_orientationAngle;
128− if (m_qmlSurface) {
133+ } else if (m_qmlSurface) {
129 return m_qmlSurface->orientationAngle();
134 return m_qmlSurface->orientationAngle();
130 } else {
135 } else {
131 return Mir::Angle0;
136 return Mir::Angle0;
@@ -134,16 +139,24 @@ Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
134
139
135void MirSurfaceItem::setOrientationAngle(Mir::OrientationAngle angle)
140void MirSurfaceItem::setOrientationAngle(Mir::OrientationAngle angle)
136{
141{
137− if (!m_qmlSurface)
142+ DEBUG_MSG(angle);
138− return;
139−
140− if (m_qmlSurface->orientationAngle() == angle)
141− return;
142
143
144+ if (m_qmlSurface) {
145+ Q_ASSERT(!m_orientationAngle);
143− m_qmlSurface->setOrientationAngle(angle);
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+ }
144
155
156+ if (m_qmlItem) {
145− QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
157+ QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
146− orientationProp.write(QVariant::fromValue(m_qmlSurface->orientationAngle()));
158+ orientationProp.write(QVariant::fromValue(orientationAngle()));
159+ }
147}
160}
148
161
149void MirSurfaceItem::updateScreenshot(QUrl screenshotUrl)
162void MirSurfaceItem::updateScreenshot(QUrl screenshotUrl)
@@ -175,6 +188,11 @@ void MirSurfaceItem::createQmlContentItem()
175 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
188 QQmlProperty screenshotSource(m_qmlItem, "screenshotSource");
176 screenshotSource.write(QVariant::fromValue(m_qmlSurface->screenshotUrl()));
189 screenshotSource.write(QVariant::fromValue(m_qmlSurface->screenshotUrl()));
177 }
190 }
191+
192+ {
193+ QQmlProperty orientationProp(m_qmlItem, "orientationAngle");
194+ orientationProp.write(QVariant::fromValue(orientationAngle()));
195+ }
178}
196}
179
197
180void MirSurfaceItem::touchEvent(QTouchEvent * event)
198void MirSurfaceItem::touchEvent(QTouchEvent * event)
@@ -255,7 +273,16 @@ void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
255 updateSurfaceSize();
273 updateSurfaceSize();
256 updateMirSurfaceVisibility();
274 updateMirSurfaceVisibility();
257
275
276+ if (m_orientationAngle) {
277+ m_qmlSurface->setOrientationAngle(*m_orientationAngle);
258− connect(m_qmlSurface, &MirSurface::orientationAngleChanged, this, &MirSurfaceItem::orientationAngleChanged);
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+
259 connect(m_qmlSurface, &MirSurface::screenshotUrlChanged, this, &MirSurfaceItem::updateScreenshot);
286 connect(m_qmlSurface, &MirSurface::screenshotUrlChanged, this, &MirSurfaceItem::updateScreenshot);
260 connect(m_qmlSurface, &MirSurface::liveChanged, this, &MirSurfaceItem::liveChanged);
287 connect(m_qmlSurface, &MirSurface::liveChanged, this, &MirSurfaceItem::liveChanged);
261 connect(m_qmlSurface, &MirSurface::stateChanged, this, &MirSurfaceItem::surfaceStateChanged);
288 connect(m_qmlSurface, &MirSurface::stateChanged, this, &MirSurfaceItem::surfaceStateChanged);
tests/mocks/Unity/Application/MirSurfaceItem.h+2
@@ -117,6 +117,8 @@ private:
117
117
118 bool m_consumesInput;
118 bool m_consumesInput;
119
119
120+ Mir::OrientationAngle *m_orientationAngle;
121+
120 int m_surfaceWidth;
122 int m_surfaceWidth;
121 int m_surfaceHeight;
123 int m_surfaceHeight;
122
124
tests/qmltests/Stages/tst_SpreadDelegate.qml+115−12
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU General Public License as published by
@@ -132,10 +132,12 @@ Rectangle {
132 property int value: selectedIndex * 90
132 property int value: selectedIndex * 90
133 }
133 }
134 Button {
134 Button {
135+ id: matchShellButton
135 text: "matchShellOrientation()"
136 text: "matchShellOrientation()"
136 onClicked: { spreadDelegateLoader.item.matchShellOrientation(); }
137 onClicked: { spreadDelegateLoader.item.matchShellOrientation(); }
137 }
138 }
138 Button {
139 Button {
140+ id: animateToShellButton
139 text: "animateToShellOrientation()"
141 text: "animateToShellOrientation()"
140 onClicked: { spreadDelegateLoader.item.animateToShellOrientation(); }
142 onClicked: { spreadDelegateLoader.item.animateToShellOrientation(); }
141 }
143 }
@@ -197,6 +199,77 @@ Rectangle {
197
199
198 }
200 }
199
201
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+
200 function test_swipeToClose_data() {
273 function test_swipeToClose_data() {
201 return [
274 return [
202 {tag: "swipeToClose=true closeable=true -> appWindow moves away",
275 {tag: "swipeToClose=true closeable=true -> appWindow moves away",
@@ -278,12 +351,12 @@ Rectangle {
278 function test_keepsSceneTransformationWhenShellRotates(data) {
351 function test_keepsSceneTransformationWhenShellRotates(data) {
279 loadWithGalleryApp.clicked();
352 loadWithGalleryApp.clicked();
280
353
354+ waitUntilAppWindowIsFullyLoaded();
355+
281 var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
356 var appWindowWithShadow = findChild(spreadDelegate, "appWindowWithShadow");
282 verify(appWindowWithShadow);
357 verify(appWindowWithShadow);
283
358
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");
287
360
288 shellOrientationAngleSelector.selectedIndex = data.selectedIndex;
361 shellOrientationAngleSelector.selectedIndex = data.selectedIndex;
289
362
@@ -293,14 +366,7 @@ Rectangle {
293
366
294
367
295 // and scene transformation must be the identity (ie, no rotation or translation)
368 // and scene transformation must be the identity (ie, no rotation or translation)
296− var pointInDelegateCoords = appWindowWithShadow.mapToItem(root, 0, 0);
369+ checkAppWindowTransformationMatchesRotation(appWindowWithShadow, 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);
304 }
370 }
305
371
306 function waitForCloseAnimationToFinish() {
372 function waitForCloseAnimationToFinish() {
@@ -316,5 +382,42 @@ Rectangle {
316 spreadDelegateLoader.item.highlightShown = true;
382 spreadDelegateLoader.item.highlightShown = true;
317 tryCompare(highlightRect, "visible", true)
383 tryCompare(highlightRect, "visible", true)
318 }
384 }
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+ }
319 }
422 }
320}
423}