Merge lp:~unity-team/unity8/shell_chrome into lp:unity8

Proposed by Michał Sawicz
Status: Merged
Approved by: Gerry Boland
Approved revision: 2215
Merged at revision: 2288
Proposed branch: lp:~unity-team/unity8/shell_chrome
Merge into: lp:unity8
Prerequisite: lp:~unity-team/unity8/side-stage-redesign
Diff against target: 862 lines (+367/-28)
20 files modified
qml/Components/InputMethod.qml (+4/-1)
qml/Shell.qml (+5/-1)
qml/Stages/AbstractStage.qml (+2/-0)
qml/Stages/DesktopStage.qml (+71/-12)
qml/Stages/PhoneStage.qml (+10/-0)
qml/Stages/StagedFullscreenPolicy.qml (+58/-0)
qml/Stages/TabletStage.qml (+9/-0)
qml/Stages/WindowResizeArea.qml (+2/-2)
qml/Stages/WindowedFullscreenPolicy.qml (+41/-0)
tests/mocks/Unity/Application/ApplicationInfo.cpp (+21/-3)
tests/mocks/Unity/Application/ApplicationInfo.h (+5/-1)
tests/mocks/Unity/Application/ApplicationManager.cpp (+6/-4)
tests/mocks/Unity/Application/MirSurface.cpp (+16/-0)
tests/mocks/Unity/Application/MirSurface.h (+5/-3)
tests/mocks/Unity/Application/MirSurfaceItem.cpp (+9/-0)
tests/mocks/Unity/Application/MirSurfaceItem.h (+1/-0)
tests/mocks/Unity/Application/Session.cpp (+26/-1)
tests/mocks/Unity/Application/Session.h (+7/-0)
tests/qmltests/Stages/tst_WindowResizeArea.qml (+7/-0)
tests/qmltests/tst_Shell.qml (+62/-0)
To merge this branch: bzr merge lp:~unity-team/unity8/shell_chrome
Reviewer Review Type Date Requested Status
Daniel d'Andrada (community) Needs Information
Gerry Boland (community) Approve
Unity8 CI Bot continuous-integration Needs Fixing
PS Jenkins bot continuous-integration Pending
Michał Sawicz Pending
Review via email: mp+288841@code.launchpad.net

This proposal supersedes a proposal from 2016-02-17.

Commit message

Add support for low shell chrome.

Description of the change

* Are there any related MPs required for this MP to build/function as expected? Please list.
https://code.launchpad.net/~nick-dedekind/unity-api/shell_chrome/+merge/286309
https://code.launchpad.net/~nick-dedekind/qtmir/shell_chrome/+merge/286307
https://code.launchpad.net/~nick-dedekind/qtubuntu/shell_chrome/+merge/286308

 * Did you perform an exploratory manual test run of your code change and any related functionality?
Yes

 * Did you make sure that your branch does not contain spurious tags?
Yes

 * If you changed the packaging (debian), did you subscribe the ubuntu-unity team to this MP?
N/A

 * If you changed the UI, has there been a design review?
No

To post a comment you must log in.
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Michał Sawicz (saviq) wrote : Posted in a previous version of this proposal

Staged mode, app fullscreen, unsets flag → remains fullscreen

When app is fullscreen and unsets flag, we should restore it.

review: Needs Fixing
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Gerry Boland (gerboland) wrote : Posted in a previous version of this proposal

+++ qml/Shell.qml
+ property string qmlComponent: {
could be readonly.

+++ qml/Stages/AbstractStage.qml
+ signal stageUnloaded
Why can you not depend on the already-existing Component.onDestruction signal? Is it happening too late?

Also when this is fired, the stage as not yet been unloaded. stageAboutToBeUnloaded would be a more accurate name :)

So I see you use it here:
+ property bool saveStateOnDestruction: true
+ Connections {
+ target: root
+ onStageUnloaded: {
+ resizeArea.saveWindowState();
+ resizeArea.saveStateOnDestruction = false;
+ fullscreenPolicy.active = false;
+ }
+ }
+ Component.onDestruction: {
+ if (saveStateOnDestruction) {
+ saveWindowState();
+ }
+ }
this is strange code. It looks like you are waiting for 2 events to save the windowState, and save on the first event. This is really strange looking, you need to justify it, and why Component.onDestruction is not enough. I do know that object destruction order is not guaranteed, is that it? If so, then why use it at all?

+++ qml/Stages/DesktopFullscreenPolicy.qml
this doesn't need to be an item, it doesn't contain anything visual. This can just be a single QtObject. You might be able to import QtQml instead of QtQuick too. lastSurface can be readonly too. Indent of the 'if" statement not clear.

I don't understand why it doesn't apply on the first surface. Could you also write a comment to explain the policy?

+++ qml/Stages/PhoneFullscreenPolicy.qml
I also suspect this could be a QtObject (am not sure if Connections will work inside QtObject though). It is odd to see "PhoneFullscreenPolicy" mentioned in TabletStage - would there be a better name? I'd also like a textual comment explaining what the policy is doing.

Why does this not have the first surface branch the Desktop policy has?

=== modified file 'qml/Stages/DesktopStage.qml'
- property alias requestedWidth: decoratedWindow.requestedWidth
- property alias requestedHeight: decoratedWindow.requestedHeight
+ property int requestedWidth: -1
+ property int requestedHeight: -1
why? You overriding the requested width/height? Or just because you animate them?

+++ tests/qmltests/tst_Shell.qml
just to confirm you mean to return the bool result of the comparison:
+ controls.focusedSurface.state === Mir.FullscreenState
Do we all know that JS returns the result of the last statement, if nothing is explicitly returned?

Revision history for this message
Gerry Boland (gerboland) wrote : Posted in a previous version of this proposal

I still need to test on devices

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Gerry Boland (gerboland) wrote :

Just did a functionality test, works as I'd expect.

lp:~unity-team/unity8/shell_chrome updated
2212. By Nick Dedekind

review fixes

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~unity-team/unity8/shell_chrome updated
2213. By Nick Dedekind

comment

Revision history for this message
Nick Dedekind (nick-dedekind) wrote :
Download full text (3.6 KiB)

>> +++ qml/Shell.qml
>> + property string qmlComponent: {
>> could be readonly.

Fixed.

>> +++ qml/Stages/AbstractStage.qml
>> + signal stageUnloaded
>> Why can you not depend on the already-existing Component.onDestruction signal? Is it happening >> too late?

onDestruction is called after the stage which replaces the destroyed stage is created. We want to perform operations before it gets replaced, or states can be changed before we save.

>> Also when this is fired, the stage as not yet been unloaded. stageAboutToBeUnloaded would be a >> more accurate name :)

Done.

>> So I see you use it here:
>> + property bool saveStateOnDestruction: true
>> + Connections {
>> + target: root
>> + onStageUnloaded: {
>> + resizeArea.saveWindowState();
>> + resizeArea.saveStateOnDestruction = false;
>> + fullscreenPolicy.active = false;
>> + }
>> + }
>> + Component.onDestruction: {
>> + if (saveStateOnDestruction) {
>> + saveWindowState();
>> + }
>> + }

>> this is strange code. It looks like you are waiting for 2 events to save the windowState, and >> save on the first event. This is really strange looking, you need to justify it, and why
>> Component.onDestruction is not enough. I do know that object destruction order is not
>> guaranteed, is that it? If so, then why use it at all?

Component.onDestruction handles the normal closing of app.
The signal handles stage replacement. And when the stage is replaced, both will be called so we guard against the latter.

>> +++ qml/Stages/DesktopFullscreenPolicy.qml
>> this doesn't need to be an item, it doesn't contain anything visual. This can just be a single
>> QtObject. You might be able to import QtQml instead of QtQuick too. lastSurface can be
>> readonly too. Indent of the 'if" statement not clear.

Done.

>> I don't understand why it doesn't apply on the first surface. Could you also write a comment
>> to explain the policy?

At the moment, the "last surface" is the one that's displayed. This will apply to all surfaces once we have surface based managament.

>> +++ qml/Stages/PhoneFullscreenPolicy.qml
>> I also suspect this could be a QtObject (am not sure if Connections will work inside QtObject
>> though). It is odd to see "PhoneFullscreenPolicy" mentioned in TabletStage - would there be a
>> better name? I'd also like a textual comment explaining what the policy is doing.

Well I would have liked to call it Staged/WindowedFullscreenPolicy. but Stage kinda means something different in gsettings & u8 :/
But I've renamed them anyway. Because that's how I roll.

>> Why does this not have the first surface branch the Desktop policy has?

The desktop policy is only applied when the surface first appears (for a switch to desktop mode).
Phone policy is always applied.

>> === modified file 'qml/Stages/DesktopStage.qml'
>> - property alias requestedWidth: decoratedWindow.requestedWidth
>> - property alias requestedHeight: decoratedWindow.requestedHeight
>> + property int requestedWidth: -1
>> + property int requestedHeight: -1
>> why? You overriding the requested width/height? Or just because you animate them?

It's a fix for sizing the window when switching stages. It previously started off at -1 through th...

Read more...

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
lp:~unity-team/unity8/shell_chrome updated
2214. By Michael Zanetti

merge with prereq

2215. By Michael Zanetti

fix whitespace

Revision history for this message
Unity8 CI Bot (unity8-ci-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Gerry Boland (gerboland) wrote :
Download full text (3.8 KiB)

> >> +++ qml/Stages/AbstractStage.qml
> >> + signal stageUnloaded
> >> Why can you not depend on the already-existing Component.onDestruction
> signal? Is it happening >> too late?
>
> onDestruction is called after the stage which replaces the destroyed stage is
> created. We want to perform operations before it gets replaced, or states can
> be changed before we save.
>
> >> Also when this is fired, the stage as not yet been unloaded.
> stageAboutToBeUnloaded would be a >> more accurate name :)
>
> Done.

Thanks.

>
> >> So I see you use it here:
> >> + property bool saveStateOnDestruction: true
> >> + Connections {
> >> + target: root
> >> + onStageUnloaded: {
> >> + resizeArea.saveWindowState();
> >> + resizeArea.saveStateOnDestruction = false;
> >> + fullscreenPolicy.active = false;
> >> + }
> >> + }
> >> + Component.onDestruction: {
> >> + if (saveStateOnDestruction) {
> >> + saveWindowState();
> >> + }
> >> + }
>
> >> this is strange code. It looks like you are waiting for 2 events to save
> the windowState, and >> save on the first event. This is really strange
> looking, you need to justify it, and why
> >> Component.onDestruction is not enough. I do know that object destruction
> order is not
> >> guaranteed, is that it? If so, then why use it at all?
>
> Component.onDestruction handles the normal closing of app.
> The signal handles stage replacement. And when the stage is replaced, both
> will be called so we guard against the latter.

I see the reasoning, but I think it sensible in a later refactoring to have a single signal we can rely on, instead of these 2.

>
> >> I don't understand why it doesn't apply on the first surface. Could you
> also write a comment
> >> to explain the policy?
>
> At the moment, the "last surface" is the one that's displayed. This will apply
> to all surfaces once we have surface based managament.

Oh I see.

>
> >> +++ qml/Stages/PhoneFullscreenPolicy.qml
> >> I also suspect this could be a QtObject (am not sure if Connections will
> work inside QtObject
> >> though). It is odd to see "PhoneFullscreenPolicy" mentioned in TabletStage
> - would there be a
> >> better name? I'd also like a textual comment explaining what the policy is
> doing.
>
> Well I would have liked to call it Staged/WindowedFullscreenPolicy. but Stage
> kinda means something different in gsettings & u8 :/
> But I've renamed them anyway. Because that's how I roll.

:)

>
> >> Why does this not have the first surface branch the Desktop policy has?
>
> The desktop policy is only applied when the surface first appears (for a
> switch to desktop mode).
> Phone policy is always applied.

Got you. That statement is nice to have in comments.

> >> === modified file 'qml/Stages/DesktopStage.qml'
> >> - property alias requestedWidth: decoratedWindow.requestedWidth
> >> - property alias requestedHeight: decoratedWindow.requestedHeight
> >> + property int requestedWidth: -1
> >> + property int requestedHeight: -1
> >> why? You overriding the requested width/height? Or just because you animate
> them?
>
> It's a fix for sizing the window when switching stages. It previously started
> off at -1 through the alias, but there were som...

Read more...

Revision history for this message
Gerry Boland (gerboland) :
review: Approve
lp:~unity-team/unity8/shell_chrome updated
2216. By Nick Dedekind

rebase

2217. By Nick Dedekind

use function for loadWindowstate

2218. By Nick Dedekind

fixed testWindowResizeArea

2219. By Nick Dedekind

test fixes

Revision history for this message
Daniel d'Andrada (dandrader) wrote :

In DesktopStage.qml:

+++ qml/Stages/DesktopStage.qml 2016-03-16 12:29:44 +0000
@@ -255,8 +255,8 @@
                 focus: appId === priv.focusedAppId
                 width: decoratedWindow.width
                 height: decoratedWindow.height
- property alias requestedWidth: decoratedWindow.requestedWidth
- property alias requestedHeight: decoratedWindow.requestedHeight
+ property int requestedWidth: -1
+ property int requestedHeight: -1
                 property alias minimumWidth: decoratedWindow.minimumWidth
                 property alias minimumHeight: decoratedWindow.minimumHeight
                 property alias maximumWidth: decoratedWindow.maximumWidth

@@ -480,12 +530,21 @@
                     active: ApplicationManager.focusedApplicationId === model.appId
                     focus: true

+ requestedWidth: appDelegate.requestedWidth
+ requestedHeight: appDelegate.requestedHeight

This boils down to the same, with the problem of leading to more code. Why the change?

review: Needs Information

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'qml/Components/InputMethod.qml'
2--- qml/Components/InputMethod.qml 2016-02-15 17:26:51 +0000
3+++ qml/Components/InputMethod.qml 2016-03-16 12:31:28 +0000
4@@ -51,7 +51,10 @@
5 }
6
7 state: {
8- if (surfaceItem.surface && surfaceItem.surfaceState != Mir.MinimizedState && root.enabled) {
9+ if (surfaceItem.surface &&
10+ surfaceItem.surfaceState != Mir.HiddenState &&
11+ surfaceItem.surfaceState != Mir.MinimizedState &&
12+ root.enabled) {
13 return "shown";
14 } else {
15 return "hidden";
16
17=== modified file 'qml/Shell.qml'
18--- qml/Shell.qml 2016-03-16 12:31:27 +0000
19+++ qml/Shell.qml 2016-03-16 12:31:28 +0000
20@@ -246,7 +246,7 @@
21 property string usageScenario: shell.usageScenario === "phone" || greeter.hasLockedApp
22 ? "phone"
23 : shell.usageScenario
24- source: {
25+ readonly property string qmlComponent: {
26 if(shell.mode === "greeter") {
27 return "Stages/ShimStage.qml"
28 } else if (applicationsDisplayLoader.usageScenario === "phone") {
29@@ -257,6 +257,10 @@
30 return "Stages/DesktopStage.qml";
31 }
32 }
33+ onQmlComponentChanged: {
34+ if (item) item.stageAboutToBeUnloaded();
35+ source = qmlComponent;
36+ }
37
38 property bool interactive: tutorial.spreadEnabled
39 && (!greeter || !greeter.shown)
40
41=== modified file 'qml/Stages/AbstractStage.qml'
42--- qml/Stages/AbstractStage.qml 2016-03-16 12:31:27 +0000
43+++ qml/Stages/AbstractStage.qml 2016-03-16 12:31:28 +0000
44@@ -51,6 +51,8 @@
45 | Qt.InvertedPortraitOrientation
46 | Qt.InvertedLandscapeOrientation
47
48+ signal stageAboutToBeUnloaded
49+
50 // Shared code for use in stage implementations
51 GSettings {
52 id: lifecycleExceptions
53
54=== modified file 'qml/Stages/DesktopStage.qml'
55--- qml/Stages/DesktopStage.qml 2016-03-10 22:37:38 +0000
56+++ qml/Stages/DesktopStage.qml 2016-03-16 12:31:28 +0000
57@@ -255,8 +255,8 @@
58 focus: appId === priv.focusedAppId
59 width: decoratedWindow.width
60 height: decoratedWindow.height
61- property alias requestedWidth: decoratedWindow.requestedWidth
62- property alias requestedHeight: decoratedWindow.requestedHeight
63+ property int requestedWidth: -1
64+ property int requestedHeight: -1
65 property alias minimumWidth: decoratedWindow.minimumWidth
66 property alias minimumHeight: decoratedWindow.minimumHeight
67 property alias maximumWidth: decoratedWindow.maximumWidth
68@@ -369,8 +369,10 @@
69 name: "fullscreen"; when: decoratedWindow.fullscreen
70 PropertyChanges {
71 target: appDelegate;
72- x: 0; y: -PanelState.panelHeight
73- requestedWidth: appContainer.width; requestedHeight: appContainer.height;
74+ x: 0;
75+ y: -PanelState.panelHeight
76+ requestedWidth: appContainer.width;
77+ requestedHeight: appContainer.height;
78 }
79 },
80 State {
81@@ -387,21 +389,42 @@
82 name: "maximized"; when: appDelegate.maximized && !appDelegate.minimized
83 PropertyChanges {
84 target: appDelegate;
85- x: root.leftMargin; y: 0;
86- requestedWidth: appContainer.width - root.leftMargin; requestedHeight: appContainer.height;
87+ x: root.leftMargin;
88+ y: 0;
89 visuallyMinimized: false;
90 visuallyMaximized: true
91 }
92+ PropertyChanges {
93+ target: decoratedWindow
94+ requestedWidth: appContainer.width - root.leftMargin;
95+ requestedHeight: appContainer.height;
96+ }
97 },
98 State {
99 name: "maximizedLeft"; when: appDelegate.maximizedLeft && !appDelegate.minimized
100- PropertyChanges { target: appDelegate; x: root.leftMargin; y: PanelState.panelHeight;
101- requestedWidth: (appContainer.width - root.leftMargin)/2; requestedHeight: appContainer.height - PanelState.panelHeight }
102+ PropertyChanges {
103+ target: appDelegate
104+ x: root.leftMargin
105+ y: PanelState.panelHeight
106+ }
107+ PropertyChanges {
108+ target: decoratedWindow
109+ requestedWidth: (appContainer.width - root.leftMargin)/2
110+ requestedHeight: appContainer.height - PanelState.panelHeight
111+ }
112 },
113 State {
114 name: "maximizedRight"; when: appDelegate.maximizedRight && !appDelegate.minimized
115- PropertyChanges { target: appDelegate; x: (appContainer.width + root.leftMargin)/2; y: PanelState.panelHeight;
116- requestedWidth: (appContainer.width - root.leftMargin)/2; requestedHeight: appContainer.height - PanelState.panelHeight }
117+ PropertyChanges {
118+ target: appDelegate;
119+ x: (appContainer.width + root.leftMargin)/2
120+ y: PanelState.panelHeight
121+ }
122+ PropertyChanges {
123+ target: decoratedWindow
124+ requestedWidth: (appContainer.width - root.leftMargin)/2
125+ requestedHeight: appContainer.height - PanelState.panelHeight
126+ }
127 },
128 State {
129 name: "minimized"; when: appDelegate.minimized
130@@ -421,13 +444,17 @@
131 enabled: appDelegate.animationsEnabled
132 PropertyAction { target: appDelegate; properties: "visuallyMinimized,visuallyMaximized" }
133 UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
134+ UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
135 },
136 Transition {
137 to: "minimized"
138 enabled: appDelegate.animationsEnabled
139 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
140 SequentialAnimation {
141- UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
142+ ParallelAnimation {
143+ UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
144+ UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
145+ }
146 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
147 ScriptAction {
148 script: {
149@@ -443,7 +470,10 @@
150 enabled: appDelegate.animationsEnabled
151 PropertyAction { target: appDelegate; property: "visuallyMinimized" }
152 SequentialAnimation {
153- UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,requestedWidth,requestedHeight,scale"; duration: UbuntuAnimation.FastDuration }
154+ ParallelAnimation {
155+ UbuntuNumberAnimation { target: appDelegate; properties: "x,y,opacity,scale"; duration: UbuntuAnimation.FastDuration }
156+ UbuntuNumberAnimation { target: decoratedWindow; properties: "requestedWidth,requestedHeight"; duration: UbuntuAnimation.FastDuration }
157+ }
158 PropertyAction { target: appDelegate; property: "visuallyMaximized" }
159 }
160 }
161@@ -458,6 +488,7 @@
162 }
163
164 WindowResizeArea {
165+ id: resizeArea
166 objectName: "windowResizeArea"
167 target: appDelegate
168 minWidth: units.gu(10)
169@@ -469,6 +500,25 @@
170 leftMargin: root.leftMargin
171
172 onPressed: { ApplicationManager.focusApplication(model.appId) }
173+
174+ Component.onCompleted: {
175+ loadWindowState();
176+ }
177+
178+ property bool saveStateOnDestruction: true
179+ Connections {
180+ target: root
181+ onStageAboutToBeUnloaded: {
182+ resizeArea.saveWindowState();
183+ resizeArea.saveStateOnDestruction = false;
184+ fullscreenPolicy.active = false;
185+ }
186+ }
187+ Component.onDestruction: {
188+ if (saveStateOnDestruction) {
189+ saveWindowState();
190+ }
191+ }
192 }
193
194 DecoratedWindow {
195@@ -480,12 +530,21 @@
196 active: ApplicationManager.focusedApplicationId === model.appId
197 focus: true
198
199+ requestedWidth: appDelegate.requestedWidth
200+ requestedHeight: appDelegate.requestedHeight
201+
202 onClose: ApplicationManager.stopApplication(model.appId)
203 onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
204 ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
205 onMinimize: appDelegate.minimize()
206 onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
207 }
208+
209+ WindowedFullscreenPolicy {
210+ id: fullscreenPolicy
211+ active: true
212+ application: decoratedWindow.application
213+ }
214 }
215 }
216 }
217
218=== modified file 'qml/Stages/PhoneStage.qml'
219--- qml/Stages/PhoneStage.qml 2016-03-10 09:19:38 +0000
220+++ qml/Stages/PhoneStage.qml 2016-03-16 12:31:28 +0000
221@@ -629,6 +629,16 @@
222 property: "focusedAppOrientationChangesEnabled"
223 value: orientationChangesEnabled
224 }
225+
226+ StagedFullscreenPolicy {
227+ id: fullscreenPolicy
228+ application: appDelegate.application
229+ }
230+
231+ Connections {
232+ target: root
233+ onStageAboutToBeUnloaded: fullscreenPolicy.active = false
234+ }
235 }
236 }
237 }
238
239=== added file 'qml/Stages/StagedFullscreenPolicy.qml'
240--- qml/Stages/StagedFullscreenPolicy.qml 1970-01-01 00:00:00 +0000
241+++ qml/Stages/StagedFullscreenPolicy.qml 2016-03-16 12:31:28 +0000
242@@ -0,0 +1,58 @@
243+/*
244+ * Copyright (C) 2016 Canonical, Ltd.
245+ *
246+ * This program is free software; you can redistribute it and/or modify
247+ * it under the terms of the GNU General Public License as published by
248+ * the Free Software Foundation; version 3.
249+ *
250+ * This program is distributed in the hope that it will be useful,
251+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
252+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
253+ * GNU General Public License for more details.
254+ *
255+ * You should have received a copy of the GNU General Public License
256+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
257+ */
258+
259+import QtQuick 2.4
260+import Unity.Application 0.1
261+
262+// This component will change the state of the surface based on the surface
263+// state and shell chrome.
264+//
265+// Chrome changed to LowChrome -> server sets client window state to "fullscreen"
266+// Chrome changed to NormalChrome -> server sets client window to "restored" state.
267+// Chrome set and state change to restored -> server RESETS client window state to "fullscreen"
268+// Chrome not set and state change to restored -> client window stays "restored"
269+// Chrome not set and state change to fulscreen -> client window stays "fullscreen"
270+QtObject {
271+ property bool active: true
272+ property QtObject application: null
273+
274+ readonly property var lastSurface: application && application.session ?
275+ application.session.lastSurface : null
276+ onLastSurfaceChanged: {
277+ if (!active || !lastSurface) return;
278+ if (lastSurface.shellChrome === Mir.LowChrome) {
279+ lastSurface.state = Mir.FullscreenState;
280+ }
281+ }
282+
283+ property var _connections: Connections {
284+ target: lastSurface
285+ onShellChromeChanged: {
286+ if (!active || !lastSurface) return;
287+ if (lastSurface.shellChrome === Mir.LowChrome) {
288+ lastSurface.state = Mir.FullscreenState;
289+ } else {
290+ lastSurface.state = Mir.RestoredState;
291+ }
292+ }
293+ onStateChanged: {
294+ if (!active) return;
295+ if (lastSurface.state === Mir.RestoredState && lastSurface.shellChrome === Mir.LowChrome) {
296+ lastSurface.state = Mir.FullscreenState;
297+ }
298+ }
299+ }
300+}
301
302=== modified file 'qml/Stages/TabletStage.qml'
303--- qml/Stages/TabletStage.qml 2016-03-16 12:31:27 +0000
304+++ qml/Stages/TabletStage.qml 2016-03-16 12:31:28 +0000
305@@ -947,6 +947,15 @@
306 period: (spreadView.positionMarker2 - spreadView.positionMarker1) / 3
307 progress: spreadTile.progress - spreadView.positionMarker1
308 }
309+
310+ StagedFullscreenPolicy {
311+ id: fullscreenPolicy
312+ application: spreadTile.application
313+ }
314+ Connections {
315+ target: root
316+ onStageAboutToBeUnloaded: fullscreenPolicy.active = false
317+ }
318 }
319 }
320 }
321
322=== modified file 'qml/Stages/WindowResizeArea.qml'
323--- qml/Stages/WindowResizeArea.qml 2016-03-10 22:43:31 +0000
324+++ qml/Stages/WindowResizeArea.qml 2016-03-16 12:31:28 +0000
325@@ -69,7 +69,7 @@
326 onHeightChanged: priv.updateNormalGeometry();
327 }
328
329- Component.onCompleted: {
330+ function loadWindowState() {
331 var windowGeometry = windowStateStorage.getGeometry(root.windowId,
332 Qt.rect(target.x, target.y, defaultWidth, defaultHeight));
333
334@@ -87,7 +87,7 @@
335 priv.updateNormalGeometry();
336 }
337
338- Component.onDestruction: {
339+ function saveWindowState() {
340 windowStateStorage.saveState(root.windowId, target.state == "maximized" ? WindowStateStorage.WindowStateMaximized : WindowStateStorage.WindowStateNormal)
341 windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight))
342 }
343
344=== added file 'qml/Stages/WindowedFullscreenPolicy.qml'
345--- qml/Stages/WindowedFullscreenPolicy.qml 1970-01-01 00:00:00 +0000
346+++ qml/Stages/WindowedFullscreenPolicy.qml 2016-03-16 12:31:28 +0000
347@@ -0,0 +1,41 @@
348+/*
349+ * Copyright (C) 2016 Canonical, Ltd.
350+ *
351+ * This program is free software; you can redistribute it and/or modify
352+ * it under the terms of the GNU General Public License as published by
353+ * the Free Software Foundation; version 3.
354+ *
355+ * This program is distributed in the hope that it will be useful,
356+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
357+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
358+ * GNU General Public License for more details.
359+ *
360+ * You should have received a copy of the GNU General Public License
361+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
362+ */
363+
364+import QtQml 2.2
365+import Unity.Application 0.1
366+
367+// This component will change the state of the surface when the stage is loaded.
368+//
369+// On first surface load; if the surface is set to low chrome & fullscreen, the
370+// state of the window is returned to restored.
371+QtObject {
372+ property bool active: true
373+ property QtObject application: null
374+
375+ readonly property var lastSurface: application && application.session ?
376+ application.session.lastSurface : null
377+ property bool _firstTimeSurface: true
378+
379+ onLastSurfaceChanged: {
380+ if (!active || !lastSurface) return;
381+ if (!_firstTimeSurface) return;
382+ _firstTimeSurface = false;
383+
384+ if (lastSurface.state === Mir.FullscreenState && lastSurface.shellChrome === Mir.LowChrome) {
385+ lastSurface.state = Mir.RestoredState;
386+ }
387+ }
388+}
389
390=== modified file 'tests/mocks/Unity/Application/ApplicationInfo.cpp'
391--- tests/mocks/Unity/Application/ApplicationInfo.cpp 2016-01-22 19:44:56 +0000
392+++ tests/mocks/Unity/Application/ApplicationInfo.cpp 2016-03-16 12:31:28 +0000
393@@ -44,6 +44,7 @@
394 , m_isTouchApp(true)
395 , m_exemptFromLifecycle(false)
396 , m_manualSurfaceCreation(false)
397+ , m_shellChrome(Mir::NormalChrome)
398 {
399 }
400
401@@ -63,6 +64,7 @@
402 , m_isTouchApp(true)
403 , m_exemptFromLifecycle(false)
404 , m_manualSurfaceCreation(false)
405+ , m_shellChrome(Mir::NormalChrome)
406 {
407 }
408
409@@ -102,9 +104,11 @@
410 if (m_session) {
411 m_session->setApplication(this);
412 m_session->setParent(this);
413+ m_session->setFullscreen(m_fullscreen);
414 SessionManager::singleton()->registerSession(m_session);
415 connect(m_session, &Session::surfaceAdded,
416 this, &ApplicationInfo::onSessionSurfaceAdded);
417+ connect(m_session, &Session::fullscreenChanged, this, &ApplicationInfo::fullscreenChanged);
418
419 if (!m_manualSurfaceCreation) {
420 QTimer::singleShot(500, m_session, &Session::createSurface);
421@@ -191,12 +195,17 @@
422
423 void ApplicationInfo::setFullscreen(bool value)
424 {
425- if (value != m_fullscreen) {
426- m_fullscreen = value;
427- Q_EMIT fullscreenChanged(value);
428+ m_fullscreen = value;
429+ if (m_session) {
430+ m_session->setFullscreen(value);
431 }
432 }
433
434+bool ApplicationInfo::fullscreen() const
435+{
436+ return m_session ? m_session->fullscreen() : m_fullscreen;
437+}
438+
439 void ApplicationInfo::setManualSurfaceCreation(bool value)
440 {
441 if (value != m_manualSurfaceCreation) {
442@@ -262,6 +271,7 @@
443 } else {
444 setState(Suspended);
445 }
446+ surface->setShellChrome(m_shellChrome);
447 }
448 }
449
450@@ -291,3 +301,11 @@
451 Q_EMIT initialSurfaceSizeChanged(m_initialSurfaceSize);
452 }
453 }
454+
455+void ApplicationInfo::setShellChrome(Mir::ShellChrome shellChrome)
456+{
457+ m_shellChrome = shellChrome;
458+ if (m_session && m_session->lastSurface()) {
459+ m_session->lastSurface()->setShellChrome(shellChrome);
460+ }
461+}
462
463=== modified file 'tests/mocks/Unity/Application/ApplicationInfo.h'
464--- tests/mocks/Unity/Application/ApplicationInfo.h 2016-01-19 21:41:34 +0000
465+++ tests/mocks/Unity/Application/ApplicationInfo.h 2016-03-16 12:31:28 +0000
466@@ -24,6 +24,7 @@
467
468 // unity-api
469 #include <unity/shell/application/ApplicationInfoInterface.h>
470+#include <unity/shell/application/Mir.h>
471
472 using namespace unity::shell::application;
473
474@@ -78,7 +79,7 @@
475 QString screenshot() const { return m_screenshotFileName; }
476
477 void setFullscreen(bool value);
478- bool fullscreen() const { return m_fullscreen; }
479+ bool fullscreen() const;
480
481 Qt::ScreenOrientations supportedOrientations() const override;
482 void setSupportedOrientations(Qt::ScreenOrientations orientations);
483@@ -97,6 +98,8 @@
484
485 QSize initialSurfaceSize() const override;
486 void setInitialSurfaceSize(const QSize &size) override;
487+
488+ Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
489 public:
490 void setSession(Session* session);
491 Session* session() const { return m_session; }
492@@ -134,6 +137,7 @@
493 QSize m_initialSurfaceSize;
494
495 bool m_manualSurfaceCreation;
496+ Mir::ShellChrome m_shellChrome;
497 };
498
499 Q_DECLARE_METATYPE(ApplicationInfo*)
500
501=== modified file 'tests/mocks/Unity/Application/ApplicationManager.cpp'
502--- tests/mocks/Unity/Application/ApplicationManager.cpp 2016-03-16 12:31:27 +0000
503+++ tests/mocks/Unity/Application/ApplicationManager.cpp 2016-03-16 12:31:28 +0000
504@@ -328,6 +328,7 @@
505 application->setName("Camera");
506 application->setScreenshotId("camera");
507 application->setIconId("camera");
508+ application->setShellChrome(Mir::LowChrome);
509 application->setFullscreen(true);
510 application->setSupportedOrientations(Qt::PortraitOrientation
511 | Qt::LandscapeOrientation
512@@ -341,7 +342,8 @@
513 application->setName("Gallery");
514 application->setScreenshotId("gallery");
515 application->setIconId("gallery");
516- application->setFullscreen(true);
517+ application->setShellChrome(Mir::LowChrome);
518+ application->setStage(ApplicationInfo::MainStage);
519 m_availableApplications.append(application);
520
521 application = new ApplicationInfo(this);
522@@ -353,7 +355,7 @@
523
524 application = new ApplicationInfo(this);
525 application->setAppId("webbrowser-app");
526- application->setFullscreen(true);
527+ application->setShellChrome(Mir::LowChrome);
528 application->setName("Browser");
529 application->setScreenshotId("browser");
530 application->setIconId("browser");
531@@ -378,7 +380,7 @@
532 application->setName("GMail");
533 application->setIconId("gmail");
534 application->setScreenshotId("gmail-webapp.svg");
535- application->setFullscreen(false);
536+ application->setStage(ApplicationInfo::MainStage);
537 application->setSupportedOrientations(Qt::PortraitOrientation
538 | Qt::LandscapeOrientation
539 | Qt::InvertedPortraitOrientation
540@@ -390,7 +392,7 @@
541 application->setName("Music");
542 application->setIconId("soundcloud");
543 application->setScreenshotId("music");
544- application->setFullscreen(false);
545+ application->setStage(ApplicationInfo::MainStage);
546 application->setSupportedOrientations(Qt::PortraitOrientation
547 | Qt::LandscapeOrientation
548 | Qt::InvertedPortraitOrientation
549
550=== modified file 'tests/mocks/Unity/Application/MirSurface.cpp'
551--- tests/mocks/Unity/Application/MirSurface.cpp 2016-03-10 22:39:10 +0000
552+++ tests/mocks/Unity/Application/MirSurface.cpp 2016-03-16 12:31:28 +0000
553@@ -38,6 +38,7 @@
554 , m_width(-1)
555 , m_height(-1)
556 , m_slowToResize(false)
557+ , m_shellChrome(Mir::NormalChrome)
558 {
559 // qDebug() << "MirSurface::MirSurface() " << name;
560 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
561@@ -134,6 +135,21 @@
562 }
563
564
565+Mir::ShellChrome MirSurface::shellChrome() const
566+{
567+ return m_shellChrome;
568+}
569+
570+void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
571+{
572+ if (shellChrome == m_shellChrome)
573+ return;
574+
575+ m_shellChrome = shellChrome;
576+ Q_EMIT shellChromeChanged(shellChrome);
577+}
578+
579+
580
581 void MirSurface::registerView(qintptr viewId)
582 {
583
584=== modified file 'tests/mocks/Unity/Application/MirSurface.h'
585--- tests/mocks/Unity/Application/MirSurface.h 2016-03-01 12:56:35 +0000
586+++ tests/mocks/Unity/Application/MirSurface.h 2016-03-16 12:31:28 +0000
587@@ -74,10 +74,13 @@
588 int widthIncrement() const override { return m_widthIncrement; }
589 int heightIncrement() const override { return m_heightIncrement; }
590
591+ Mir::ShellChrome shellChrome() const override;
592+
593 ////
594 // API for tests
595
596 Q_INVOKABLE void setLive(bool live);
597+ Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
598
599 void registerView(qintptr viewId);
600 void unregisterView(qintptr viewId);
601@@ -109,9 +112,6 @@
602 void setActiveFocus(bool);
603
604 Q_SIGNALS:
605- void stateChanged(Mir::State);
606- void liveChanged(bool live);
607- void orientationAngleChanged(Mir::OrientationAngle angle);
608 void widthChanged();
609 void heightChanged();
610 void slowToResizeChanged();
611@@ -152,6 +152,8 @@
612 QSize m_delayedResize;
613 QSize m_pendingResize;
614
615+ Mir::ShellChrome m_shellChrome;
616+
617 struct View {
618 bool visible;
619 };
620
621=== modified file 'tests/mocks/Unity/Application/MirSurfaceItem.cpp'
622--- tests/mocks/Unity/Application/MirSurfaceItem.cpp 2016-03-16 12:31:27 +0000
623+++ tests/mocks/Unity/Application/MirSurfaceItem.cpp 2016-03-16 12:31:28 +0000
624@@ -100,6 +100,15 @@
625 }
626 }
627
628+Mir::ShellChrome MirSurfaceItem::shellChrome() const
629+{
630+ if (m_qmlSurface) {
631+ return m_qmlSurface->shellChrome();
632+ } else {
633+ return Mir::NormalChrome;
634+ }
635+}
636+
637 Mir::OrientationAngle MirSurfaceItem::orientationAngle() const
638 {
639 if (m_qmlSurface) {
640
641=== modified file 'tests/mocks/Unity/Application/MirSurfaceItem.h'
642--- tests/mocks/Unity/Application/MirSurfaceItem.h 2016-03-16 12:31:27 +0000
643+++ tests/mocks/Unity/Application/MirSurfaceItem.h 2016-03-16 12:31:28 +0000
644@@ -47,6 +47,7 @@
645 Mir::Type type() const override;
646 QString name() const override;
647 bool live() const override;
648+ Mir::ShellChrome shellChrome() const override;
649
650 Mir::State surfaceState() const override;
651 void setSurfaceState(Mir::State) override {}
652
653=== modified file 'tests/mocks/Unity/Application/Session.cpp'
654--- tests/mocks/Unity/Application/Session.cpp 2015-12-01 12:17:24 +0000
655+++ tests/mocks/Unity/Application/Session.cpp 2016-03-16 12:31:28 +0000
656@@ -34,6 +34,7 @@
657 , m_surface(nullptr)
658 , m_parentSession(nullptr)
659 , m_children(new SessionModel(this))
660+ , m_fullscreen(false)
661 {
662 // qDebug() << "Session::Session() " << this->name();
663
664@@ -71,6 +72,25 @@
665 deleteLater();
666 }
667
668+void Session::updateFullscreenProperty()
669+{
670+ if (m_surfaces.rowCount() > 0) {
671+ // TODO: Figure out something better
672+ setFullscreen(lastSurface()->state() == Mir::FullscreenState);
673+ } else {
674+ // Keep the current value of the fullscreen property until we get a new
675+ // surface
676+ }
677+}
678+
679+void Session::setFullscreen(bool fullscreen)
680+{
681+ if (m_fullscreen != fullscreen) {
682+ m_fullscreen = fullscreen;
683+ Q_EMIT fullscreenChanged(m_fullscreen);
684+ }
685+}
686+
687 void Session::setApplication(ApplicationInfo* application)
688 {
689 if (m_application == application)
690@@ -82,15 +102,18 @@
691
692 void Session::appendSurface(MirSurface* surface)
693 {
694- // qDebug() << "Session::appendSurface - session=" << name() << "surface=" << surface;
695+ qDebug() << "Session::appendSurface - session=" << name() << "surface=" << surface;
696
697 m_surfaces.insert(m_surfaces.rowCount(), surface);
698
699+ connect(surface, &MirSurfaceInterface::stateChanged, this, &Session::updateFullscreenProperty);
700 connect(surface, &QObject::destroyed,
701 this, [this, surface]() { this->removeSurface(surface); });
702
703 Q_EMIT lastSurfaceChanged(surface);
704 Q_EMIT surfaceAdded(surface);
705+
706+ updateFullscreenProperty();
707 }
708
709 void Session::removeSurface(MirSurface* surface)
710@@ -99,6 +122,8 @@
711 if (m_surfaces.contains(surface)) {
712 m_surfaces.remove(surface);
713 }
714+
715+ updateFullscreenProperty();
716 }
717
718 void Session::setScreenshot(const QUrl& screenshot)
719
720=== modified file 'tests/mocks/Unity/Application/Session.h'
721--- tests/mocks/Unity/Application/Session.h 2015-12-01 12:17:24 +0000
722+++ tests/mocks/Unity/Application/Session.h 2016-03-16 12:31:28 +0000
723@@ -47,6 +47,7 @@
724 //getters
725 QString name() const { return m_name; }
726 bool live() const { return m_live; }
727+ bool fullscreen() const { return m_fullscreen; }
728 ApplicationInfo* application() const { return m_application; }
729 MirSurface *lastSurface() const;
730 ObjectListModel<MirSurface>* surfaces() const;
731@@ -57,6 +58,7 @@
732 void removeSurface(MirSurface* surface);
733 void setScreenshot(const QUrl& m_screenshot);
734 void setLive(bool live);
735+ void setFullscreen(bool fullscreen);
736
737 Q_INVOKABLE void addChildSession(Session* session);
738 void insertChildSession(uint index, Session* session);
739@@ -68,6 +70,7 @@
740 void liveChanged(bool live);
741 void surfaceAdded(MirSurface *surface);
742 void lastSurfaceChanged(MirSurface *surface);
743+ void fullscreenChanged(bool fullscreen);
744
745 // internal mock use
746 void deregister();
747@@ -75,6 +78,9 @@
748 public Q_SLOTS:
749 Q_INVOKABLE void createSurface();
750
751+private Q_SLOTS:
752+ void updateFullscreenProperty();
753+
754 private:
755 SessionModel* childSessions() const;
756 void setParentSession(Session* session);
757@@ -87,6 +93,7 @@
758 Session* m_parentSession;
759 SessionModel* m_children;
760 ObjectListModel<MirSurface> m_surfaces;
761+ bool m_fullscreen;
762
763 friend class ApplicationTestInterface;
764 };
765
766=== modified file 'tests/qmltests/Stages/tst_WindowResizeArea.qml'
767--- tests/qmltests/Stages/tst_WindowResizeArea.qml 2015-11-30 18:25:47 +0000
768+++ tests/qmltests/Stages/tst_WindowResizeArea.qml 2016-03-16 12:31:28 +0000
769@@ -72,6 +72,13 @@
770 windowId: "test-window-id"
771 screenWidth: root.width
772 screenHeight: root.height
773+
774+ Component.onCompleted: {
775+ loadWindowState();
776+ }
777+ Component.onDestruction: {
778+ saveWindowState();
779+ }
780 }
781
782 Rectangle {
783
784=== modified file 'tests/qmltests/tst_Shell.qml'
785--- tests/qmltests/tst_Shell.qml 2016-03-10 22:43:31 +0000
786+++ tests/qmltests/tst_Shell.qml 2016-03-16 12:31:28 +0000
787@@ -131,6 +131,9 @@
788 anchors.right: root.right
789 width: units.gu(30)
790
791+ property var focusedApp: ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
792+ property var focusedSurface: focusedApp && focusedApp.session ? focusedApp.session.lastSurface : null
793+
794 Rectangle {
795 id: controlRect
796 anchors { left: parent.left; right: parent.right }
797@@ -251,6 +254,65 @@
798 appId: modelData
799 }
800 }
801+
802+ Label { text: "Focused Application"; font.bold: true }
803+
804+ Row {
805+ CheckBox {
806+ id: fullscreeAppCheck
807+
808+ onTriggered: {
809+ if (!controls.focusedSurface) return;
810+ if (controls.focusedSurface.state == Mir.FullscreenState) {
811+ controls.focusedSurface.state = Mir.RestoredState;
812+ } else {
813+ controls.focusedSurface.state = Mir.FullscreenState;
814+ }
815+ }
816+
817+ Binding {
818+ target: fullscreeAppCheck
819+ when: controls.focusedSurface
820+ property: "checked"
821+ value: {
822+ if (!controls.focusedSurface) return false;
823+ return controls.focusedSurface.state === Mir.FullscreenState
824+ }
825+ }
826+ }
827+ Label {
828+ text: "Fullscreen"
829+ }
830+ }
831+
832+ Row {
833+ CheckBox {
834+ id: chromeAppCheck
835+
836+ onTriggered: {
837+ if (!controls.focusedSurface) return;
838+ if (controls.focusedSurface.shellChrome == Mir.LowChrome) {
839+ controls.focusedSurface.setShellChrome(Mir.NormalChrome);
840+ } else {
841+ controls.focusedSurface.setShellChrome(Mir.LowChrome);
842+ }
843+ }
844+
845+ Binding {
846+ target: chromeAppCheck
847+ when: controls.focusedSurface !== null
848+ property: "checked"
849+ value: {
850+ if (!controls.focusedSurface) return false;
851+ controls.focusedSurface.shellChrome === Mir.LowChrome
852+ }
853+ }
854+ }
855+ Label {
856+ text: "Low Chrome"
857+ }
858+ }
859+
860 }
861 }
862 }

Subscribers

People subscribed via source and target branches