Merge lp:~lukas-kde/unity8/activateWindows into lp:unity8

Proposed by Lukáš Tinkl
Status: Superseded
Proposed branch: lp:~lukas-kde/unity8/activateWindows
Merge into: lp:unity8
Prerequisite: lp:~unity-team/unity8/mousePointer
Diff against target: 1013 lines (+523/-77)
14 files modified
plugins/Utils/windowstatestorage.cpp (+62/-23)
plugins/Utils/windowstatestorage.h (+13/-1)
qml/Components/WindowControlButtons.qml (+3/-0)
qml/Panel/Panel.qml (+5/-3)
qml/Shell.qml (+2/-0)
qml/Stages/DesktopSpreadDelegate.qml (+2/-2)
qml/Stages/DesktopStage.qml (+152/-13)
qml/Stages/WindowDecoration.qml (+2/-1)
qml/Stages/WindowResizeArea.qml (+38/-7)
tests/mocks/Utils/windowstatestorage.cpp (+11/-0)
tests/mocks/Utils/windowstatestorage.h (+9/-0)
tests/qmltests/Stages/tst_DesktopStage.qml (+120/-25)
tests/qmltests/Stages/tst_WindowResizeArea.qml (+35/-2)
tests/qmltests/tst_Shell.qml (+69/-0)
To merge this branch: bzr merge lp:~lukas-kde/unity8/activateWindows
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Michael Zanetti Pending
Daniel d'Andrada Pending
Review via email: mp+274903@code.launchpad.net

This proposal supersedes a proposal from 2015-10-19.

This proposal has been superseded by a proposal from 2015-10-26.

Commit message

Restore windows when activating from the spread, maintain a focus stack

Stop displaying the "grabbing" icon when we merely click to focus the app's decoration.

Provide keyboard shortcuts for common window operations

Description of the change

Raise/restore windows when activating from the spread, maintain a focus stack in case we minimize/close an app.

Stop displaying the "grabbing" icon when we merely click to focus the app's decoration.

Provide keyboard shortcuts for common window operations

* Are there any related MPs required for this MP to build/function as expected? Please list.

https://code.launchpad.net/~unity-team/unity8/mousePointer/+merge/273369 as a prereq

* 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?

Yes

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

N/A

To post a comment you must log in.
Revision history for this message
Daniel d'Andrada (dandrader) wrote : Posted in a previous version of this proposal

Please follow the commit message format as explained here: https://wiki.ubuntu.com/Process/Merges/Checklists/Unity8

Revision history for this message
Lukáš Tinkl (lukas-kde) wrote : Posted in a previous version of this proposal

> Please follow the commit message format as explained here:
> https://wiki.ubuntu.com/Process/Merges/Checklists/Unity8

Should be fine now

Revision history for this message
Daniel d'Andrada (dandrader) wrote : Posted in a previous version of this proposal

Could you please add qml tests to cover those use cases you mention (ie active focus when clicking on decoration and raise/restore when activation from spread)?

I believe we already have a test for the first one (focus when clicking decoration). We had similar problems in the past before. Should investigate why it passes now even though there's a bug there (maybe it tests only with touches and not with mouse clicks, don't know).

review: Needs Fixing
Revision history for this message
Daniel d'Andrada (dandrader) wrote : Posted in a previous version of this proposal

Oh, and it's worth making lp:~unity-team/unity8/mousePointer from silo 022 a prerequisite as it makes a lot of changes in this code.

Revision history for this message
Daniel d'Andrada (dandrader) wrote : Posted in a previous version of this proposal

> > Please follow the commit message format as explained here:
> > https://wiki.ubuntu.com/Process/Merges/Checklists/Unity8
>
> Should be fine now

Yes, thanks!

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal

FAILED: Continuous integration, rev:2005
http://jenkins.qa.ubuntu.com/job/unity8-ci/6488/
Executed test runs:
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-vivid-touch/4701
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-wily-touch/870
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-phablet-qmluitests-vivid/1200
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-phablet-qmluitests-wily/516
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity8-vivid-amd64-ci/1095
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity8-vivid-i386-ci/1096
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity8-wily-amd64-ci/727
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity8-wily-i386-ci/728
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-runner-vivid-mako/3794
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/4698
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/4698/artifact/work/output/*zip*/output.zip
    SUCCESS: http://s-jenkins.ubuntu-ci:8080/job/touch-flash-device/24339
    UNSTABLE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-runner-wily-mako/514
    SUCCESS: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-wily-armhf/870
        deb: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-wily-armhf/870/artifact/work/output/*zip*/output.zip
    SUCCESS: http://s-jenkins.ubuntu-ci:8080/job/touch-flash-device/24336

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/unity8-ci/6488/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Michael Zanetti (mzanetti) :
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~lukas-kde/unity8/activateWindows updated
2021. By Lukáš Tinkl

merge trunk

2022. By Lukáš Tinkl

merge lp:~mzanetti/unity8/panel-button-fixes

2023. By Lukáš Tinkl

display the maximized window's title in the panel, next to the buttons

2024. By Lukáš Tinkl

merge prereq to fix failing tests

2025. By Lukáš Tinkl

elide the window title in the title bar

2026. By Lukáš Tinkl

silence warnings

2027. By Lukáš Tinkl

fix issues found by mzanetti

additionally properly restore windows from minimized state to the correct
previous state (maximized, maximizedLeft/Right)

2028. By Lukáš Tinkl

fix restoring the apps from spread to the correct state and size

potential fix for the Alt+F4 shortcut problem

2029. By Lukáš Tinkl

fix click to focus

2030. By Lukáš Tinkl

take the panel height into account when semimaximizing windows

2031. By Lukáš Tinkl

add a test to prove smashing all the 4 cursor keys together does nothing :)

2032. By Lukáš Tinkl

make sure to play the minimized animation before switching focus to next

2033. By Lukáš Tinkl

merge trunk

2034. By Lukáš Tinkl

cleanup

2035. By Lukáš Tinkl

cleanup

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'plugins/Utils/windowstatestorage.cpp'
2--- plugins/Utils/windowstatestorage.cpp 2015-09-14 09:11:08 +0000
3+++ plugins/Utils/windowstatestorage.cpp 2015-10-26 13:53:58 +0000
4@@ -47,26 +47,38 @@
5 m_db.close();
6 }
7
8+void WindowStateStorage::saveState(const QString &windowId, WindowStateStorage::WindowState state)
9+{
10+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO state (windowId, state) values ('%1', '%2');")
11+ .arg(windowId)
12+ .arg((int)state);
13+
14+ saveValue(queryString);
15+}
16+
17+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue) const
18+{
19+ const QString queryString = QStringLiteral("SELECT * FROM state WHERE windowId = '%1';")
20+ .arg(windowId);
21+
22+ QSqlQuery query = getValue(queryString);
23+
24+ if (!query.first()) {
25+ return defaultValue;
26+ }
27+ return (WindowState)query.value("state").toInt();
28+}
29+
30 void WindowStateStorage::saveGeometry(const QString &windowId, const QRect &rect)
31 {
32- QMutexLocker mutexLocker(&s_mutex);
33-
34- QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
35+ const QString queryString = QStringLiteral("INSERT OR REPLACE INTO geometry (windowId, x, y, width, height) values ('%1', '%2', '%3', '%4', '%5');")
36 .arg(windowId)
37 .arg(rect.x())
38 .arg(rect.y())
39 .arg(rect.width())
40 .arg(rect.height());
41
42- QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
43- m_asyncQueries.append(future);
44-
45- QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
46- futureWatcher->setFuture(future);
47- connect(futureWatcher, &QFutureWatcher<void>::finished,
48- this,
49- [=](){ m_asyncQueries.removeAll(futureWatcher->future());
50- futureWatcher->deleteLater(); });
51+ saveValue(queryString);
52 }
53
54 void WindowStateStorage::executeAsyncQuery(const QString &queryString)
55@@ -82,20 +94,13 @@
56 }
57 }
58
59-QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue)
60+QRect WindowStateStorage::getGeometry(const QString &windowId, const QRect &defaultValue) const
61 {
62- QMutexLocker l(&s_mutex);
63 QString queryString = QStringLiteral("SELECT * FROM geometry WHERE windowId = '%1';")
64 .arg(windowId);
65- QSqlQuery query;
66-
67- bool ok = query.exec(queryString);
68- if (!ok) {
69- qWarning() << "Error retrieving window state for" << windowId
70- << "Driver error:" << query.lastError().driverText()
71- << "Database error:" << query.lastError().databaseText();
72- return defaultValue;
73- }
74+
75+ QSqlQuery query = getValue(queryString);
76+
77 if (!query.first()) {
78 return defaultValue;
79 }
80@@ -114,4 +119,38 @@
81 QSqlQuery query;
82 query.exec(QStringLiteral("CREATE TABLE geometry(windowId TEXT UNIQUE, x INTEGER, y INTEGER, width INTEGER, height INTEGER);"));
83 }
84+
85+ if (!m_db.tables().contains("state")) {
86+ QSqlQuery query;
87+ query.exec("CREATE TABLE state(windowId TEXT UNIQUE, state INTEGER);");
88+ }
89+}
90+
91+void WindowStateStorage::saveValue(const QString &queryString)
92+{
93+ QMutexLocker mutexLocker(&s_mutex);
94+
95+ QFuture<void> future = QtConcurrent::run(executeAsyncQuery, queryString);
96+ m_asyncQueries.append(future);
97+
98+ QFutureWatcher<void> *futureWatcher = new QFutureWatcher<void>();
99+ futureWatcher->setFuture(future);
100+ connect(futureWatcher, &QFutureWatcher<void>::finished,
101+ this,
102+ [=](){ m_asyncQueries.removeAll(futureWatcher->future());
103+ futureWatcher->deleteLater(); });
104+}
105+
106+QSqlQuery WindowStateStorage::getValue(const QString &queryString) const
107+{
108+ QMutexLocker l(&s_mutex);
109+ QSqlQuery query;
110+
111+ bool ok = query.exec(queryString);
112+ if (!ok) {
113+ qWarning() << "Error retrieving database query:" << queryString
114+ << "Driver error:" << query.lastError().driverText()
115+ << "Database error:" << query.lastError().databaseText();
116+ }
117+ return query;
118 }
119
120=== modified file 'plugins/Utils/windowstatestorage.h'
121--- plugins/Utils/windowstatestorage.h 2015-03-13 19:01:32 +0000
122+++ plugins/Utils/windowstatestorage.h 2015-10-26 13:53:58 +0000
123@@ -22,16 +22,28 @@
124 class WindowStateStorage: public QObject
125 {
126 Q_OBJECT
127+ Q_ENUMS(WindowState)
128 public:
129+ enum WindowState {
130+ WindowStateNormal,
131+ WindowStateMaximized
132+ };
133+
134 WindowStateStorage(QObject *parent = 0);
135 virtual ~WindowStateStorage();
136
137+ Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
138+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue) const;
139+
140 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
141- Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
142+ Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue) const;
143
144 private:
145 void initdb();
146
147+ void saveValue(const QString &queryString);
148+ QSqlQuery getValue(const QString &queryString) const;
149+
150 static void executeAsyncQuery(const QString &queryString);
151 static QMutex s_mutex;
152
153
154=== modified file 'qml/Components/WindowControlButtons.qml'
155--- qml/Components/WindowControlButtons.qml 2014-11-24 11:21:38 +0000
156+++ qml/Components/WindowControlButtons.qml 2015-10-26 13:53:58 +0000
157@@ -28,6 +28,7 @@
158 signal maximize()
159
160 Rectangle {
161+ objectName: "closeWindowButton"
162 height: parent.height; width: height; radius: height / 2
163 gradient: Gradient {
164 GradientStop { color: "#F49073"; position: 0 }
165@@ -38,6 +39,7 @@
166 MouseArea { anchors.fill: parent; onClicked: root.close() }
167 }
168 Rectangle {
169+ objectName: "minimizeWindowButton"
170 height: parent.height; width: height; radius: height / 2
171 gradient: Gradient {
172 GradientStop { color: "#92918C"; position: 0 }
173@@ -48,6 +50,7 @@
174 MouseArea { anchors.fill: parent; onClicked: root.minimize() }
175 }
176 Rectangle {
177+ objectName: "maximizeWindowButton"
178 height: parent.height; width: height; radius: height / 2
179 gradient: Gradient {
180 GradientStop { color: "#92918C"; position: 0 }
181
182=== modified file 'qml/Panel/Panel.qml'
183--- qml/Panel/Panel.qml 2015-09-29 12:48:46 +0000
184+++ qml/Panel/Panel.qml 2015-10-26 13:53:58 +0000
185@@ -28,6 +28,7 @@
186 property alias callHint: __callHint
187 property bool fullscreenMode: false
188 property real indicatorAreaShowProgress: 1.0
189+ property bool locked: false
190
191 opacity: fullscreenMode && indicators.fullyClosed ? 0.0 : 1.0
192
193@@ -136,7 +137,7 @@
194 }
195
196 shown: false
197- width: root.width - (PanelState.buttonsVisible ? windowControlButtons.width : 0)
198+ width: root.width - (windowControlButtons.visible ? windowControlButtons.width : 0)
199 minimizedPanelHeight: units.gu(3)
200 expandedPanelHeight: units.gu(7)
201 openedHeight: root.height - indicatorOrangeLine.height
202@@ -164,13 +165,14 @@
203
204 WindowControlButtons {
205 id: windowControlButtons
206+ objectName: "panelWindowControlButtons"
207 anchors {
208 left: parent.left
209 top: parent.top
210 margins: units.gu(0.7)
211 }
212 height: indicators.minimizedPanelHeight - anchors.margins * 2
213- visible: PanelState.buttonsVisible
214+ visible: PanelState.buttonsVisible && !root.locked
215 onClose: PanelState.close()
216 onMinimize: PanelState.minimize()
217 onMaximize: PanelState.maximize()
218@@ -189,7 +191,7 @@
219 id: __callHint
220 anchors {
221 top: parent.top
222- left: PanelState.buttonsVisible ? windowControlButtons.right : parent.left
223+ left: windowControlButtons.visible ? windowControlButtons.right : parent.left
224 }
225 height: indicators.minimizedPanelHeight
226 visible: active && indicators.state == "initial"
227
228=== modified file 'qml/Shell.qml'
229--- qml/Shell.qml 2015-10-16 17:11:54 +0000
230+++ qml/Shell.qml 2015-10-26 13:53:58 +0000
231@@ -57,6 +57,7 @@
232 property bool beingResized
233 property string usageScenario: "phone" // supported values: "phone", "tablet" or "desktop"
234 property string mode: "full-greeter"
235+ property bool cursorVisible: false
236 function updateFocusedAppOrientation() {
237 applicationsDisplayLoader.item.updateFocusedAppOrientation();
238 }
239@@ -536,6 +537,7 @@
240
241 fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
242 || greeter.hasLockedApp
243+ locked: greeter && greeter.active
244 }
245
246 Launcher {
247
248=== modified file 'qml/Stages/DesktopSpreadDelegate.qml'
249--- qml/Stages/DesktopSpreadDelegate.qml 2015-09-18 11:03:48 +0000
250+++ qml/Stages/DesktopSpreadDelegate.qml 2015-10-26 13:53:58 +0000
251@@ -29,8 +29,8 @@
252 property bool highlightShown: false
253 property real shadowOpacity: 1
254
255- property int windowWidth: application.session && application.session.surface ? application.session.surface.size.width : 0
256- property int windowHeight: application.session && application.session.surface ? application.session.surface.size.height : 0
257+ property int windowWidth: application && application.session && application.session.surface ? application.session.surface.size.width : 0
258+ property int windowHeight: application && application.session && application.session.surface ? application.session.surface.size.height : 0
259
260 state: "normal"
261 states: [
262
263=== modified file 'qml/Stages/DesktopStage.qml'
264--- qml/Stages/DesktopStage.qml 2015-10-19 14:27:57 +0000
265+++ qml/Stages/DesktopStage.qml 2015-10-26 13:53:58 +0000
266@@ -24,6 +24,7 @@
267 import "../Components/PanelState"
268 import Utils 0.1
269 import Ubuntu.Gestures 0.1
270+import GlobalShortcut 1.0
271
272 Rectangle {
273 id: root
274@@ -63,7 +64,15 @@
275 spread.state = "";
276 }
277
278- ApplicationManager.requestFocusApplication(appId)
279+ ApplicationManager.focusApplication(appId);
280+ }
281+
282+ onApplicationRemoved: {
283+ priv.removeAndFocusPreviousInStack(appId);
284+ }
285+
286+ onFocusedApplicationIdChanged: {
287+ priv.addToFocusStack(priv.focusedAppId);
288 }
289
290 onFocusRequested: {
291@@ -78,6 +87,53 @@
292 }
293 }
294
295+ GlobalShortcut {
296+ id: closeWindowShortcut
297+ shortcut: Qt.AltModifier|Qt.Key_F4
298+ onTriggered: ApplicationManager.stopApplication(priv.focusedAppId)
299+ active: priv.focusedAppId !== ""
300+ }
301+
302+ GlobalShortcut {
303+ id: showSpreadShortcut
304+ shortcut: Qt.MetaModifier|Qt.Key_W
305+ onTriggered: spread.state = "altTab"
306+ }
307+
308+ GlobalShortcut {
309+ id: minimizeAllShortcut
310+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_D
311+ onTriggered: priv.minimizeAllWindows()
312+ }
313+
314+ GlobalShortcut {
315+ id: maximizeWindowShortcut
316+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Up
317+ onTriggered: priv.focusedAppDelegate.minimized ? priv.focusedAppDelegate.restore() : priv.focusedAppDelegate.maximize()
318+ active: priv.focusedAppDelegate !== null
319+ }
320+
321+ GlobalShortcut {
322+ id: maximizeWindowLeftShortcut
323+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Left
324+ onTriggered: priv.focusedAppDelegate.maximizeLeft()
325+ active: priv.focusedAppDelegate !== null
326+ }
327+
328+ GlobalShortcut {
329+ id: maximizeWindowRightShortcut
330+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Right
331+ onTriggered: priv.focusedAppDelegate.maximizeRight()
332+ active: priv.focusedAppDelegate !== null
333+ }
334+
335+ GlobalShortcut {
336+ id: minimizeRestoreShortcut
337+ shortcut: Qt.MetaModifier|Qt.ControlModifier|Qt.Key_Down
338+ onTriggered: priv.focusedAppDelegate.maximized ? priv.focusedAppDelegate.restore() : priv.focusedAppDelegate.minimize()
339+ active: priv.focusedAppDelegate !== null
340+ }
341+
342 QtObject {
343 id: priv
344
345@@ -86,6 +142,11 @@
346 var index = indexOf(focusedAppId);
347 return index >= 0 && index < appRepeater.count ? appRepeater.itemAt(index) : null
348 }
349+ onFocusedAppDelegateChanged: { // restore the window from minimization when we focus it (e.g. using spread)
350+ if (focusedAppDelegate && focusedAppDelegate.minimized) {
351+ focusedAppDelegate.restore();
352+ }
353+ }
354
355 function indexOf(appId) {
356 for (var i = 0; i < ApplicationManager.count; i++) {
357@@ -95,6 +156,45 @@
358 }
359 return -1;
360 }
361+
362+ property var focusStack: [] // focus stack of appIds
363+
364+ function addToFocusStack(appId) {
365+ var oldIndex = focusStack.indexOf(appId);
366+ if (oldIndex != -1) {
367+ // remove the old item
368+ focusStack.splice(oldIndex, 1);
369+ }
370+ // insert to the top of the focus stack
371+ focusStack.unshift(appId);
372+ }
373+
374+ function removeAndFocusPreviousInStack(appId) {
375+ var removedIndex = focusStack.indexOf(appId);
376+ if (removedIndex != -1) {
377+ focusStack.splice(removedIndex, 1); // remove one item from the focus stack
378+ focusFirstInStack(); // focus the first one
379+ }
380+ }
381+
382+ function focusFirstInStack() {
383+ var newHead = focusStack[0];
384+ if (newHead !== "") {
385+ ApplicationManager.focusApplication(newHead);
386+ }
387+ }
388+
389+ function minimizeAllWindows() {
390+ focusStack.forEach(function(appId) {
391+ var appDelegate = appRepeater.itemAt(indexOf(appId));
392+ if (appDelegate && !appDelegate.minimized) {
393+ // we don't want to change the focus to a different window
394+ appDelegate.minimized = true;
395+ }
396+ });
397+ ApplicationManager.unfocusCurrentApplication(); // no app should have focus at this point
398+ focusStack = [];
399+ }
400 }
401
402 Connections {
403@@ -103,14 +203,15 @@
404 ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
405 }
406 onMinimize: appRepeater.itemAt(0).minimize();
407- onMaximize: appRepeater.itemAt(0).unmaximize();
408+ onMaximize: appRepeater.itemAt(0).restore();
409 }
410
411 Binding {
412 target: PanelState
413 property: "buttonsVisible"
414- value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.state === "maximized"
415+ value: priv.focusedAppDelegate !== null && priv.focusedAppDelegate.maximized
416 }
417+ Component.onDestruction: PanelState.buttonsVisible = false;
418
419 FocusScope {
420 id: appContainer
421@@ -132,18 +233,23 @@
422
423 delegate: FocusScope {
424 id: appDelegate
425+ objectName: "appDelegate_" + appId
426 z: ApplicationManager.count - index
427 y: units.gu(3)
428 width: units.gu(60)
429 height: units.gu(50)
430- focus: model.appId === priv.focusedAppId
431+ focus: appId === priv.focusedAppId
432
433 property bool maximized: false
434+ property bool maximizedLeft: false
435+ property bool maximizedRight: false
436 property bool minimized: false
437+ readonly property string appId: model.appId
438+ property bool animationsEnabled: true
439
440 onFocusChanged: {
441- if (focus && ApplicationManager.focusedApplicationId !== model.appId) {
442- ApplicationManager.focusApplication(model.appId);
443+ if (focus && ApplicationManager.focusedApplicationId !== appId) {
444+ ApplicationManager.focusApplication(appId);
445 }
446 }
447
448@@ -158,36 +264,69 @@
449 value: ApplicationInfoInterface.RequestedRunning // Always running for now
450 }
451
452- function maximize() {
453+ function maximize(animated) {
454+ animationsEnabled = (animated === undefined) || animated;
455 minimized = false;
456 maximized = true;
457- }
458- function minimize() {
459+ maximizedLeft = false;
460+ maximizedRight = false;
461+ }
462+ function maximizeLeft() {
463+ minimized = false;
464+ maximized = false;
465+ maximizedLeft = true;
466+ maximizedRight = false;
467+ }
468+ function maximizeRight() {
469+ minimized = false;
470+ maximized = false;
471+ maximizedLeft = false;
472+ maximizedRight = true;
473+ }
474+ function minimize(animated) {
475+ animationsEnabled = (animated === undefined) || animated;
476 maximized = false;
477 minimized = true;
478+ maximizedLeft = false;
479+ maximizedRight = false;
480+ priv.removeAndFocusPreviousInStack(appId);
481 }
482- function unmaximize() {
483+ function restore(animated) {
484+ animationsEnabled = (animated === undefined) || animated;
485 minimized = false;
486 maximized = false;
487+ maximizedLeft = false;
488+ maximizedRight = false;
489+ priv.addToFocusStack(appId);
490 }
491
492 states: [
493 State {
494 name: "normal"; when: !appDelegate.maximized && !appDelegate.minimized
495+ && !appDelegate.maximizedLeft && !appDelegate.maximizedRight
496 },
497 State {
498 name: "maximized"; when: appDelegate.maximized
499 PropertyChanges { target: appDelegate; x: 0; y: 0; width: root.width; height: root.height }
500 },
501 State {
502+ name: "maximized_left"; when: appDelegate.maximizedLeft
503+ PropertyChanges { target: appDelegate; x: 0; y: units.gu(3); width: root.width/2; height: root.height }
504+ },
505+ State {
506+ name: "maximized_right"; when: appDelegate.maximizedRight
507+ PropertyChanges { target: appDelegate; x: root.width/2; y: units.gu(3); width: root.width/2; height: root.height }
508+ },
509+ State {
510 name: "minimized"; when: appDelegate.minimized
511 PropertyChanges { target: appDelegate; x: -appDelegate.width / 2; scale: units.gu(5) / appDelegate.width; opacity: 0 }
512 }
513 ]
514 transitions: [
515 Transition {
516- from: "maximized,minimized,normal,"
517- to: "maximized,minimized,normal,"
518+ from: "maximized,maximized_left,maximized_right,minimized,normal,"
519+ to: "maximized,maximized_left,maximized_right,minimized,normal,"
520+ enabled: appDelegate.animationsEnabled
521 PropertyAnimation { target: appDelegate; properties: "x,y,opacity,width,height,scale" }
522 },
523 Transition {
524@@ -234,7 +373,7 @@
525 focus: true
526
527 onClose: ApplicationManager.stopApplication(model.appId)
528- onMaximize: appDelegate.maximize()
529+ onMaximize: appDelegate.maximized ? appDelegate.restore() : appDelegate.maximize()
530 onMinimize: appDelegate.minimize()
531 onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
532 }
533
534=== modified file 'qml/Stages/WindowDecoration.qml'
535--- qml/Stages/WindowDecoration.qml 2015-10-19 19:05:23 +0000
536+++ qml/Stages/WindowDecoration.qml 2015-10-26 13:53:58 +0000
537@@ -45,14 +45,15 @@
538 priv.distanceX = pos.x;
539 priv.distanceY = pos.y;
540 priv.dragging = true;
541- Mir.cursorName = "grabbing";
542 } else {
543 priv.dragging = false;
544 Mir.cursorName = "";
545 }
546 }
547+
548 onPositionChanged: {
549 if (priv.dragging) {
550+ Mir.cursorName = "grabbing";
551 var pos = mapToItem(root.target.parent, mouseX, mouseY);
552 root.target.x = pos.x - priv.distanceX;
553 root.target.y = pos.y - priv.distanceY;
554
555=== modified file 'qml/Stages/WindowResizeArea.qml'
556--- qml/Stages/WindowResizeArea.qml 2015-09-29 12:48:46 +0000
557+++ qml/Stages/WindowResizeArea.qml 2015-10-26 13:53:58 +0000
558@@ -36,18 +36,49 @@
559 property int minWidth: 0
560 property int minHeight: 0
561
562+ QtObject {
563+ id: priv
564+
565+ property int normalX: 0
566+ property int normalY: 0
567+ property int normalWidth: 0
568+ property int normalHeight: 0
569+
570+ function updateNormalGeometry() {
571+ if (root.target.state == "normal") {
572+ normalX = root.target.x
573+ normalY = root.target.y
574+ normalWidth = root.target.width
575+ normalHeight = root.target.height
576+ }
577+ }
578+ }
579+
580+ Connections {
581+ target: root.target
582+ onXChanged: priv.updateNormalGeometry();
583+ onYChanged: priv.updateNormalGeometry();
584+ onWidthChanged: priv.updateNormalGeometry();
585+ onHeightChanged: priv.updateNormalGeometry();
586+ }
587+
588 Component.onCompleted: {
589- var windowState = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
590- if (windowState !== undefined) {
591- target.x = windowState.x
592- target.y = windowState.y
593- target.width = windowState.width
594- target.height = windowState.height
595+ var windowGeometry = windowStateStorage.getGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
596+ if (windowGeometry !== undefined) {
597+ target.x = windowGeometry.x
598+ target.y = windowGeometry.y
599+ target.width = windowGeometry.width
600+ target.height = windowGeometry.height
601+ }
602+ var windowState = windowStateStorage.getState(root.windowId, WindowStateStorage.WindowStateNormal)
603+ if (windowState === WindowStateStorage.WindowStateMaximized) {
604+ target.maximize(false)
605 }
606 }
607
608 Component.onDestruction: {
609- windowStateStorage.saveGeometry(root.windowId, Qt.rect(target.x, target.y, target.width, target.height))
610+ windowStateStorage.saveState(root.windowId, target.state == "maximized" ? WindowStateStorage.WindowStateMaximized : WindowStateStorage.WindowStateNormal)
611+ windowStateStorage.saveGeometry(root.windowId, Qt.rect(priv.normalX, priv.normalY, priv.normalWidth, priv.normalHeight))
612 }
613
614 QtObject {
615
616=== modified file 'tests/mocks/Utils/windowstatestorage.cpp'
617--- tests/mocks/Utils/windowstatestorage.cpp 2015-08-24 15:39:53 +0000
618+++ tests/mocks/Utils/windowstatestorage.cpp 2015-10-26 13:53:58 +0000
619@@ -44,3 +44,14 @@
620 if (!m_geometry.contains(windowId)) return defaultValue;
621 return m_geometry.value(windowId).toRect();
622 }
623+
624+void WindowStateStorage::saveState(const QString &windowId, WindowState state)
625+{
626+ m_state[windowId] = state;
627+}
628+
629+WindowStateStorage::WindowState WindowStateStorage::getState(const QString &windowId, WindowStateStorage::WindowState defaultValue)
630+{
631+ if (!m_state.contains(windowId)) return defaultValue;
632+ return m_state.value(windowId);
633+}
634
635=== modified file 'tests/mocks/Utils/windowstatestorage.h'
636--- tests/mocks/Utils/windowstatestorage.h 2015-08-24 15:39:53 +0000
637+++ tests/mocks/Utils/windowstatestorage.h 2015-10-26 13:53:58 +0000
638@@ -22,9 +22,17 @@
639 {
640 Q_OBJECT
641 Q_PROPERTY(QVariantMap geometry READ geometry WRITE setGeometry NOTIFY geometryChanged)
642+ Q_ENUMS(WindowState)
643 public:
644+ enum WindowState {
645+ WindowStateNormal,
646+ WindowStateMaximized
647+ };
648 WindowStateStorage(QObject *parent = 0);
649
650+ Q_INVOKABLE void saveState(const QString &windowId, WindowState state);
651+ Q_INVOKABLE WindowState getState(const QString &windowId, WindowState defaultValue);
652+
653 Q_INVOKABLE void saveGeometry(const QString &windowId, const QRect &rect);
654 Q_INVOKABLE QRect getGeometry(const QString &windowId, const QRect &defaultValue);
655
656@@ -35,5 +43,6 @@
657 void setGeometry(const QVariantMap& geometry);
658 QVariantMap geometry() const;
659
660+ QHash<QString, WindowState> m_state;
661 QVariantMap m_geometry;
662 };
663
664=== modified file 'tests/qmltests/Stages/tst_DesktopStage.qml'
665--- tests/qmltests/Stages/tst_DesktopStage.qml 2015-09-17 12:25:29 +0000
666+++ tests/qmltests/Stages/tst_DesktopStage.qml 2015-10-26 13:53:58 +0000
667@@ -53,14 +53,10 @@
668
669 focus: true
670
671- property bool itemDestroyed: false
672 sourceComponent: Component {
673 DesktopStage {
674 color: "darkblue"
675 anchors.fill: parent
676- Component.onDestruction: {
677- desktopStageLoader.itemDestroyed = true;
678- }
679 }
680 }
681 }
682@@ -95,16 +91,10 @@
683 property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
684
685 function cleanup() {
686- desktopStageLoader.itemDestroyed = false;
687 desktopStageLoader.active = false;
688
689 tryCompare(desktopStageLoader, "status", Loader.Null);
690 tryCompare(desktopStageLoader, "item", null);
691- // Loader.status might be Loader.Null and Loader.item might be null but the Loader
692- // actually took place. Likely because Loader waits until the next event loop
693- // iteration to do its work. So to ensure the reload, we will wait until the
694- // Shell instance gets destroyed.
695- tryCompare(desktopStageLoader, "itemDestroyed", true);
696
697 killAllRunningApps();
698
699@@ -117,7 +107,7 @@
700 var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
701 ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
702 }
703- compare(ApplicationManager.count, 1)
704+ compare(ApplicationManager.count, 1);
705 }
706
707 function waitUntilAppSurfaceShowsUp(appId) {
708@@ -147,10 +137,7 @@
709 }
710
711 function test_appFocusSwitch(data) {
712- var i;
713- for (i = 0; i < data.apps.length; i++) {
714- startApplication(data.apps[i]);
715- }
716+ data.apps.forEach(startApplication);
717
718 ApplicationManager.requestFocusApplication(data.apps[data.focusfrom]);
719 tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.surface, "activeFocus", true);
720@@ -167,10 +154,7 @@
721 }
722
723 function test_tappingOnWindowChangesFocusedApp(data) {
724- var i;
725- for (i = 0; i < data.apps.length; i++) {
726- startApplication(data.apps[i]);
727- }
728+ data.apps.forEach(startApplication);
729 var fromAppId = data.apps[data.focusfrom];
730 var toAppId = data.apps[data.focusTo]
731
732@@ -187,18 +171,37 @@
733 compare(ApplicationManager.focusedApplicationId, toAppId);
734 }
735
736+ function test_clickingOnWindowChangesFocusedApp_data() {
737+ return test_tappingOnWindowChangesFocusedApp_data(); // reuse test data
738+ }
739+
740+ function test_clickingOnWindowChangesFocusedApp(data) {
741+ data.apps.forEach(startApplication);
742+ var fromAppId = data.apps[data.focusfrom];
743+ var toAppId = data.apps[data.focusTo]
744+
745+ var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
746+ verify(fromAppWindow);
747+ mouseClick(fromAppWindow);
748+ compare(fromAppWindow.application.session.surface.activeFocus, true);
749+ compare(ApplicationManager.focusedApplicationId, fromAppId);
750+
751+ var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
752+ verify(toAppWindow);
753+ mouseClick(toAppWindow);
754+ compare(toAppWindow.application.session.surface.activeFocus, true);
755+ compare(ApplicationManager.focusedApplicationId, toAppId);
756+ }
757+
758 function test_tappingOnDecorationFocusesApplication_data() {
759 return [
760- {tag: "dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 0, focusTo: 1 },
761- {tag: "dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 1, focusTo: 0 },
762+ {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 0, focusTo: 1 },
763+ {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 1, focusTo: 0 },
764 ]
765 }
766
767 function test_tappingOnDecorationFocusesApplication(data) {
768- var i;
769- for (i = 0; i < data.apps.length; i++) {
770- startApplication(data.apps[i]);
771- }
772+ data.apps.forEach(startApplication);
773
774 var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
775 verify(fromAppDecoration);
776@@ -210,5 +213,97 @@
777 tap(toAppDecoration);
778 tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.surface, "activeFocus", true);
779 }
780+
781+ function test_clickingOnDecorationFocusesApplication_data() {
782+ return test_tappingOnDecorationFocusesApplication_data(); // reuse test data
783+ }
784+
785+ function test_clickingOnDecorationFocusesApplication(data) {
786+ data.apps.forEach(startApplication);
787+
788+ var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
789+ verify(fromAppDecoration);
790+ mouseClick(fromAppDecoration);
791+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.surface, "activeFocus", true);
792+
793+ var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
794+ verify(toAppDecoration);
795+ mouseClick(toAppDecoration);
796+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.surface, "activeFocus", true);
797+ }
798+
799+ function test_windowMaximize() {
800+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
801+ apps.forEach(startApplication);
802+ var appName = "dialer-app";
803+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
804+ verify(appDelegate);
805+ ApplicationManager.focusApplication(appName);
806+ keyClick(Qt.Key_Up, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Up shortcut to maximize
807+ tryCompare(appDelegate, "maximized", true);
808+ tryCompare(appDelegate, "minimized", false);
809+ }
810+
811+ function test_windowMaximizeLeft() {
812+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
813+ apps.forEach(startApplication);
814+ var appName = "dialer-app";
815+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
816+ verify(appDelegate);
817+ ApplicationManager.focusApplication(appName);
818+ keyClick(Qt.Key_Left, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Left shortcut to maximizeLeft
819+ tryCompare(appDelegate, "maximized", false);
820+ tryCompare(appDelegate, "minimized", false);
821+ tryCompare(appDelegate, "maximizedLeft", true);
822+ tryCompare(appDelegate, "maximizedRight", false);
823+ }
824+
825+ function test_windowMaximizeRight() {
826+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
827+ apps.forEach(startApplication);
828+ var appName = "dialer-app";
829+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
830+ verify(appDelegate);
831+ ApplicationManager.focusApplication(appName);
832+ keyClick(Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Right shortcut to maximizeRight
833+ tryCompare(appDelegate, "maximized", false);
834+ tryCompare(appDelegate, "minimized", false);
835+ tryCompare(appDelegate, "maximizedLeft", false);
836+ tryCompare(appDelegate, "maximizedRight", true);
837+ }
838+
839+ function test_windowMinimize() {
840+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
841+ apps.forEach(startApplication);
842+ var appName = "dialer-app";
843+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
844+ verify(appDelegate);
845+ ApplicationManager.focusApplication(appName);
846+ keyClick(Qt.Key_Down, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Down shortcut to minimize
847+ tryCompare(appDelegate, "maximized", false);
848+ tryCompare(appDelegate, "minimized", true);
849+ verify(ApplicationManager.focusedApplicationId != ""); // verify we don't lose focus when minimizing an app
850+ }
851+
852+ function test_windowMinimizeAll() {
853+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
854+ apps.forEach(startApplication);
855+ verify(ApplicationManager.count == 3);
856+ keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
857+ tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
858+ }
859+
860+ function test_windowClose() {
861+ var apps = ["unity8-dash", "dialer-app", "camera-app"];
862+ apps.forEach(startApplication);
863+ verify(ApplicationManager.count == 3);
864+ var appName = "dialer-app";
865+ var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
866+ verify(appDelegate);
867+ ApplicationManager.focusApplication(appName);
868+ keyClick(Qt.Key_F4, Qt.AltModifier); // Alt+F4 shortcut to close
869+ verify(ApplicationManager.count == 2); // verify the app is gone
870+ verify(ApplicationManager.findApplication(appName) === null); // and it's not in running apps
871+ }
872 }
873 }
874
875=== modified file 'tests/qmltests/Stages/tst_WindowResizeArea.qml'
876--- tests/qmltests/Stages/tst_WindowResizeArea.qml 2015-10-08 14:27:12 +0000
877+++ tests/qmltests/Stages/tst_WindowResizeArea.qml 2015-10-26 13:53:58 +0000
878@@ -44,8 +44,11 @@
879 width: units.gu(20)
880 property int windowHeight: height
881 property int windowWidth: width
882- onWindowHeightChanged: height = windowHeight
883- onWindowWidthChanged: width = windowWidth
884+ state: "normal"
885+
886+ function maximize() {
887+ state = "maximized"
888+ }
889
890 WindowResizeArea {
891 id: windowResizeArea
892@@ -195,5 +198,35 @@
893 tryCompare(fakeWindow, "width", initialWindowWidth);
894 tryCompare(fakeWindow, "height", initialWindowHeight);
895 }
896+
897+ function test_saveRestoreMaximized() {
898+ var initialWindowX = fakeWindow.x;
899+ var initialWindowY = fakeWindow.y;
900+
901+ var moveDelta = units.gu(5);
902+
903+ fakeWindow.x = initialWindowX + moveDelta
904+ fakeWindow.y = initialWindowY + moveDelta
905+
906+ // Now change the state to maximized. The window should not keep updating the stored values
907+ fakeWindow.state = "maximized"
908+ fakeWindow.x = 31415 // 0 is too risky to pass the test even when broken
909+ fakeWindow.y = 31415
910+
911+ // This will destroy the window and recreate it
912+ windowLoader.active = false;
913+ waitForRendering(root);
914+ windowLoader.active = true;
915+
916+ // Make sure it's again where we left it in normal state before destroying
917+ tryCompare(fakeWindow, "x", initialWindowX + moveDelta)
918+ tryCompare(fakeWindow, "y", initialWindowX + moveDelta)
919+
920+ // Make sure maximize() has been called after restoring
921+ tryCompare(fakeWindow, "state", "maximized")
922+
923+ // clean up
924+ fakeWindow.state = "normal"
925+ }
926 }
927 }
928
929=== modified file 'tests/qmltests/tst_Shell.qml'
930--- tests/qmltests/tst_Shell.qml 2015-10-20 08:10:03 +0000
931+++ tests/qmltests/tst_Shell.qml 2015-10-26 13:53:58 +0000
932@@ -31,6 +31,7 @@
933 import Wizard 0.1 as Wizard
934
935 import "../../qml"
936+import "../../qml/Components/PanelState"
937 import "Stages"
938
939 Rectangle {
940@@ -1707,5 +1708,73 @@
941
942 keyRelease(Qt.Key_Control);
943 }
944+
945+ // regression test for http://pad.lv/1443319
946+ function test_closeMaximizedAndRestart() {
947+ loadDesktopShellWithApps();
948+
949+ var appRepeater = findChild(shell, "appRepeater")
950+ var appId = ApplicationManager.get(0).appId;
951+ var appDelegate = appRepeater.itemAt(0);
952+ var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
953+
954+ tryCompare(appDelegate, "state", "normal");
955+ tryCompare(PanelState, "buttonsVisible", false)
956+
957+ mouseClick(maximizeButton, maximizeButton.width / 2, maximizeButton.height / 2);
958+ tryCompare(appDelegate, "state", "maximized");
959+ tryCompare(PanelState, "buttonsVisible", true)
960+
961+ ApplicationManager.stopApplication(appId);
962+ tryCompare(PanelState, "buttonsVisible", false)
963+
964+ ApplicationManager.startApplication(appId);
965+ tryCompare(PanelState, "buttonsVisible", true)
966+ }
967+
968+ // bug http://pad.lv/1431566
969+ function test_switchToStagedHidesPanelButtons() {
970+ loadDesktopShellWithApps();
971+ var appRepeater = findChild(shell, "appRepeater")
972+ var appId = ApplicationManager.get(0).appId;
973+ var appDelegate = appRepeater.itemAt(0);
974+ var panelButtons = findChild(shell, "panelWindowControlButtons")
975+
976+ tryCompare(appDelegate, "state", "normal");
977+ tryCompare(panelButtons, "visible", false);
978+
979+ appDelegate.maximize(false);
980+ tryCompare(panelButtons, "visible", true);
981+
982+ shell.usageScenario = "phone";
983+ waitForRendering(shell);
984+ tryCompare(panelButtons, "visible", false);
985+
986+ shell.usageScenario = "desktop";
987+ waitForRendering(shell);
988+ tryCompare(panelButtons, "visible", true);
989+ }
990+
991+ function test_lockingGreeterHidesPanelButtons() {
992+ loadDesktopShellWithApps();
993+ var appRepeater = findChild(shell, "appRepeater")
994+ var appId = ApplicationManager.get(0).appId;
995+ var appDelegate = appRepeater.itemAt(0);
996+ var panelButtons = findChild(shell, "panelWindowControlButtons")
997+
998+ tryCompare(appDelegate, "state", "normal");
999+ tryCompare(panelButtons, "visible", false);
1000+
1001+ appDelegate.maximize(false);
1002+ tryCompare(panelButtons, "visible", true);
1003+
1004+ LightDM.Greeter.showGreeter();
1005+ waitForRendering(shell);
1006+ tryCompare(panelButtons, "visible", false);
1007+
1008+ LightDM.Greeter.hideGreeter();
1009+ waitForRendering(shell);
1010+ tryCompare(panelButtons, "visible", true);
1011+ }
1012 }
1013 }

Subscribers

People subscribed via source and target branches