on xenial help sign in
Surface-based window management

- We no longer deal with the Session concept. - Each application has a list of top-level surfaces and each surface has a list of prompt surfaces (general support for child surfaces not yet implemented) - Stages (desktop, phone and tablet) work on a TopLevelSurfaceList model instead of ApplicationManager. - TopLevelSurfaceList contains all the top-level surfaces from all running (or suspended) applications Approved by: Gerry Boland, Lukáš Tinkl

commitfdf1715778172e415319bf4d095c0adfc14a1c25 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateThu 21 Apr 2016, 13:47 (11 years ago) parent665b045e97d5b6
79 files changed+4280−3054
debian/control+1−1 debian/unity8-private.install+1 plugins/CMakeLists.txt+1 plugins/WindowManager/CMakeLists.txt+13 plugins/WindowManager/TopLevelSurfaceList.cpp+486 plugins/WindowManager/TopLevelSurfaceList.h+223 plugins/WindowManager/WindowManagerPlugin.cpp+28 tests/mocks/Unity/Application/SessionManager.h → plugins/WindowManager/WindowManagerPlugin.h+10−22 plugins/WindowManager/qmldir+2 qml/Components/KeymapSwitcher.qml+1−9 qml/Greeter/Greeter.qml+5−4 qml/Launcher/LauncherPanel.qml+1 qml/Shell.qml+67−53 qml/Stages/AbstractStage.qml+9−2 qml/Stages/Animations/BaseSessionAnimation.qml−96 qml/Stages/Animations/SwipeFromBottomAnimation.qml−53 qml/Stages/ApplicationWindow.qml+110−68 qml/Stages/DecoratedWindow.qml+5−5 qml/Stages/DesktopSpread.qml+22−18 qml/Stages/DesktopSpreadDelegate.qml+4−3 qml/Stages/DesktopStage.qml+159−98 qml/Stages/PhoneStage.qml+152−89 qml/Stages/PromptSurfaceAnimations.qml+81 qml/Stages/SessionContainer.qml−180 qml/Stages/SpreadDelegate.qml+4−3 qml/Stages/StagedFullscreenPolicy.qml+12−14 qml/Stages/SurfaceContainer.qml+97−40 qml/Stages/TabletStage.qml+300−237 qml/Stages/TopLevelSurfaceRepeater.qml+52 qml/Stages/TransformedSpreadDelegate.qml+4−4 qml/Stages/TransformedTabletSpreadDelegate.qml+29−23 qml/Stages/WindowedFullscreenPolicy.qml+5−7 qml/Tutorial/TutorialBottom.qml+1−1 tests/mocks/Unity/Application/ApplicationInfo.cpp+181−108 tests/mocks/Unity/Application/ApplicationInfo.h+39−24 tests/mocks/Unity/Application/ApplicationManager.cpp+109−74 tests/mocks/Unity/Application/ApplicationManager.h+12−14 tests/mocks/Unity/Application/ApplicationTestInterface.cpp−117 tests/mocks/Unity/Application/ApplicationTestInterface.h−45 tests/mocks/Unity/Application/CMakeLists.txt+3−4 tests/mocks/Unity/Application/MirSurface.cpp+175−10 tests/mocks/Unity/Application/MirSurface.h+51−7 tests/mocks/Unity/Application/MirSurfaceItem.cpp+13−7 tests/mocks/Unity/Application/MirSurfaceListModel.cpp+121 tests/mocks/Unity/Application/MirSurfaceListModel.h+56 tests/mocks/Unity/Application/ObjectListModel.h+2 tests/mocks/Unity/Application/Session.cpp−209 tests/mocks/Unity/Application/Session.h−104 tests/mocks/Unity/Application/SessionManager.cpp−55 tests/mocks/Unity/Application/SessionModel.h−35 tests/mocks/Unity/Application/SurfaceManager.cpp+16−9 tests/mocks/Unity/Application/SurfaceManager.h+5−2 tests/mocks/Unity/Application/UbuntuKeyboardInfo.cpp−2 tests/mocks/Unity/Application/UbuntuKeyboardInfo.h−9 tests/mocks/Unity/Application/VirtualKeyboard.cpp+2−3 tests/mocks/Unity/Application/VirtualKeyboard.h+1−1 tests/mocks/Unity/Application/plugin.cpp+32−35 tests/mocks/Utils/CMakeLists.txt+4 tests/mocks/Utils/plugin.cpp+1−1 tests/plugins/Unity/Launcher/launchermodeltest.cpp+2−3 tests/qmltests/CMakeLists.txt−1 tests/qmltests/Dash/tst_DashShell.qml+6−2 tests/qmltests/Greeter/tst_Greeter.qml+2−2 tests/qmltests/Panel/tst_ActiveCallHint.qml+3−2 tests/qmltests/Panel/tst_Panel.qml+2−2 tests/qmltests/Stages/ApplicationCheckBox.qml+104−58 tests/qmltests/Stages/RecursingChildSessionControl.qml+10−61 tests/qmltests/Stages/tst_ApplicationWindow.qml+73−59 tests/qmltests/Stages/tst_DesktopStage.qml+243−161 tests/qmltests/Stages/tst_PhoneStage.qml+272−82 tests/qmltests/Stages/tst_SessionContainer.qml−331 tests/qmltests/Stages/tst_SpreadDelegate.qml+3−2 tests/qmltests/Stages/tst_SurfaceContainer.qml+242−46 tests/qmltests/Stages/tst_TabletStage.qml+252−89 tests/qmltests/Tutorial/tst_Tutorial.qml+4−8 tests/qmltests/tst_OrientedShell.qml+92−65 tests/qmltests/tst_Shell.qml+233−160 tests/qmltests/tst_ShellWithPin.qml+9−13 tests/utils/modules/Unity/Test/UnityTestCase.qml+20−2
debian/control+1−1
@@ -30,7 +30,7 @@ Build-Depends: cmake,
3030 libqt5xmlpatterns5-dev,
3131 libsystemsettings-dev,
3232 libudev-dev,
33− libunity-api-dev (>= 7.108),
33+ libunity-api-dev (>= 7.110),
3434 libusermetricsoutput1-dev,
3535# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
3636 libx11-dev[!armhf],
debian/unity8-private.install+1
@@ -14,6 +14,7 @@ usr/lib/*/unity8/qml/Ubuntu
1414usr/lib/*/unity8/qml/Unity
1515usr/lib/*/unity8/qml/UInput
1616usr/lib/*/unity8/qml/Utils
17+usr/lib/*/unity8/qml/WindowManager
1718usr/lib/*/unity8/qml/Wizard
1819usr/share/accountsservice/interfaces
1920usr/share/dbus-1/interfaces
plugins/CMakeLists.txt+1
@@ -25,4 +25,5 @@ add_subdirectory(Ubuntu)
2525add_subdirectory(UInput)
2626add_subdirectory(Unity)
2727add_subdirectory(Utils)
28+add_subdirectory(WindowManager)
2829add_subdirectory(Wizard)
plugins/WindowManager/CMakeLists.txtnew file+13
@@ -0,0 +1,13 @@
1+set(WINDOWMANAGER_SRC
2+ TopLevelSurfaceList.cpp
3+ WindowManagerPlugin.cpp
4+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
5+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
6+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
7+ )
8+
9+add_library(windowmanager-qml SHARED ${WINDOWMANAGER_SRC})
10+
11+qt5_use_modules(windowmanager-qml Qml Quick Gui)
12+
13+add_unity8_plugin(WindowManager 0.1 WindowManager TARGETS windowmanager-qml)
plugins/WindowManager/TopLevelSurfaceList.cppnew file+486
@@ -0,0 +1,486 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TopLevelSurfaceList.h"
18+
19+// unity-api
20+#include <unity/shell/application/ApplicationInfoInterface.h>
21+#include <unity/shell/application/MirSurfaceInterface.h>
22+#include <unity/shell/application/MirSurfaceListInterface.h>
23+
24+#include <QMetaObject>
25+
26+Q_LOGGING_CATEGORY(UNITY_TOPSURFACELIST, "unity.topsurfacelist", QtDebugMsg)
27+
28+#define DEBUG_MSG qCDebug(UNITY_TOPSURFACELIST).nospace().noquote() << __func__
29+
30+using namespace unity::shell::application;
31+
32+TopLevelSurfaceList::TopLevelSurfaceList(QObject *parent) :
33+ QAbstractListModel(parent)
34+{
35+ DEBUG_MSG << "()";
36+}
37+
38+TopLevelSurfaceList::~TopLevelSurfaceList()
39+{
40+ DEBUG_MSG << "()";
41+}
42+
43+int TopLevelSurfaceList::rowCount(const QModelIndex &parent) const
44+{
45+ return !parent.isValid() ? m_surfaceList.size() : 0;
46+}
47+
48+QVariant TopLevelSurfaceList::data(const QModelIndex& index, int role) const
49+{
50+ if (index.row() < 0 || index.row() >= m_surfaceList.size())
51+ return QVariant();
52+
53+ if (role == SurfaceRole) {
54+ MirSurfaceInterface *surface = m_surfaceList.at(index.row()).surface;
55+ return QVariant::fromValue(surface);
56+ } else if (role == ApplicationRole) {
57+ return QVariant::fromValue(m_surfaceList.at(index.row()).application);
58+ } else if (role == IdRole) {
59+ return QVariant::fromValue(m_surfaceList.at(index.row()).id);
60+ } else {
61+ return QVariant();
62+ }
63+}
64+
65+void TopLevelSurfaceList::raise(MirSurfaceInterface *surface)
66+{
67+ if (!surface)
68+ return;
69+
70+ DEBUG_MSG << "(MirSurface[" << (void*)surface << "])";
71+
72+ int i = indexOf(surface);
73+ if (i != -1) {
74+ raiseId(m_surfaceList.at(i).id);
75+ }
76+}
77+
78+void TopLevelSurfaceList::appendPlaceholder(ApplicationInfoInterface *application)
79+{
80+ DEBUG_MSG << "(" << application->appId() << ")";
81+
82+ appendSurfaceHelper(nullptr, application);
83+}
84+
85+void TopLevelSurfaceList::appendSurface(MirSurfaceInterface *surface, ApplicationInfoInterface *application)
86+{
87+ Q_ASSERT(surface != nullptr);
88+
89+ bool filledPlaceholder = false;
90+ for (int i = 0; i < m_surfaceList.count() && !filledPlaceholder; ++i) {
91+ ModelEntry &entry = m_surfaceList[i];
92+ if (entry.application == application && entry.surface == nullptr) {
93+ entry.surface = surface;
94+ connectSurface(surface);
95+ DEBUG_MSG << " appId=" << application->appId() << " surface=" << surface
96+ << ", filling out placeholder. after: " << toString();
97+ Q_EMIT dataChanged(index(i) /* topLeft */, index(i) /* bottomRight */, QVector<int>() << SurfaceRole);
98+ filledPlaceholder = true;
99+ }
100+ }
101+
102+ if (!filledPlaceholder) {
103+ DEBUG_MSG << " appId=" << application->appId() << " surface=" << surface << ", adding new row";
104+ appendSurfaceHelper(surface, application);
105+ }
106+}
107+
108+void TopLevelSurfaceList::appendSurfaceHelper(MirSurfaceInterface *surface, ApplicationInfoInterface *application)
109+{
110+ if (m_modelState == IdleState) {
111+ m_modelState = InsertingState;
112+ beginInsertRows(QModelIndex(), m_surfaceList.size() /*first*/, m_surfaceList.size() /*last*/);
113+ } else {
114+ Q_ASSERT(m_modelState == ResettingState);
115+ // No point in signaling anything if we're resetting the whole model
116+ }
117+
118+ int id = generateId();
119+ m_surfaceList.append(ModelEntry(surface, application, id));
120+ if (surface) {
121+ connectSurface(surface);
122+ }
123+
124+ if (m_modelState == InsertingState) {
125+ endInsertRows();
126+ Q_EMIT countChanged();
127+ Q_EMIT listChanged();
128+ m_modelState = IdleState;
129+ }
130+
131+ DEBUG_MSG << " after " << toString();
132+}
133+
134+void TopLevelSurfaceList::connectSurface(MirSurfaceInterface *surface)
135+{
136+ connect(surface, &MirSurfaceInterface::focusedChanged, this, [this, surface](bool focused){
137+ if (focused) {
138+ this->raise(surface);
139+ }
140+ });
141+ connect(surface, &MirSurfaceInterface::liveChanged, this, [this, surface](bool live){
142+ if (!live) {
143+ onSurfaceDied(surface);
144+ }
145+ });
146+ connect(surface, &QObject::destroyed, this, [this, surface](){ this->onSurfaceDestroyed(surface); });
147+}
148+
149+void TopLevelSurfaceList::onSurfaceDied(MirSurfaceInterface *surface)
150+{
151+ int i = indexOf(surface);
152+ if (i == -1) {
153+ return;
154+ }
155+
156+ auto application = m_surfaceList[i].application;
157+
158+ // can't be starting if it already has a surface
159+ Q_ASSERT(application->state() != ApplicationInfoInterface::Starting);
160+
161+ if (application->state() == ApplicationInfoInterface::Running) {
162+ m_surfaceList[i].removeOnceSurfaceDestroyed = true;
163+ } else {
164+ // assume it got killed by the out-of-memory daemon.
165+ //
166+ // So leave entry in the model and only remove its surface, so shell can display a screenshot
167+ // in its place.
168+ m_surfaceList[i].removeOnceSurfaceDestroyed = false;
169+ }
170+}
171+
172+void TopLevelSurfaceList::onSurfaceDestroyed(MirSurfaceInterface *surface)
173+{
174+ int i = indexOf(surface);
175+ if (i == -1) {
176+ return;
177+ }
178+
179+ if (m_surfaceList[i].removeOnceSurfaceDestroyed) {
180+ removeAt(i);
181+ } else {
182+ m_surfaceList[i].surface = nullptr;
183+ Q_EMIT dataChanged(index(i) /* topLeft */, index(i) /* bottomRight */, QVector<int>() << SurfaceRole);
184+ DEBUG_MSG << " Removed surface from entry. After: " << toString();
185+ }
186+}
187+
188+void TopLevelSurfaceList::removeAt(int index)
189+{
190+ if (m_modelState == IdleState) {
191+ beginRemoveRows(QModelIndex(), index, index);
192+ m_modelState = RemovingState;
193+ } else {
194+ Q_ASSERT(m_modelState == ResettingState);
195+ // No point in signaling anything if we're resetting the whole model
196+ }
197+
198+ m_surfaceList.removeAt(index);
199+
200+ if (m_modelState == RemovingState) {
201+ endRemoveRows();
202+ Q_EMIT countChanged();
203+ Q_EMIT listChanged();
204+ m_modelState = IdleState;
205+ }
206+
207+ DEBUG_MSG << " after " << toString();
208+}
209+
210+int TopLevelSurfaceList::indexOf(MirSurfaceInterface *surface)
211+{
212+ for (int i = 0; i < m_surfaceList.count(); ++i) {
213+ if (m_surfaceList.at(i).surface == surface) {
214+ return i;
215+ }
216+ }
217+ return -1;
218+}
219+
220+void TopLevelSurfaceList::move(int from, int to)
221+{
222+ if (from == to) return;
223+ DEBUG_MSG << " from=" << from << " to=" << to;
224+
225+ if (from >= 0 && from < m_surfaceList.size() && to >= 0 && to < m_surfaceList.size()) {
226+ QModelIndex parent;
227+ /* When moving an item down, the destination index needs to be incremented
228+ by one, as explained in the documentation:
229+ http://qt-project.org/doc/qt-5.0/qtcore/qabstractitemmodel.html#beginMoveRows */
230+
231+ Q_ASSERT(m_modelState == IdleState);
232+ m_modelState = MovingState;
233+
234+ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
235+ m_surfaceList.move(from, to);
236+ endMoveRows();
237+ Q_EMIT listChanged();
238+
239+ m_modelState = IdleState;
240+
241+ DEBUG_MSG << " after " << toString();
242+ }
243+}
244+
245+MirSurfaceInterface *TopLevelSurfaceList::surfaceAt(int index) const
246+{
247+ if (index >=0 && index < m_surfaceList.count()) {
248+ return m_surfaceList[index].surface;
249+ } else {
250+ return nullptr;
251+ }
252+}
253+
254+ApplicationInfoInterface *TopLevelSurfaceList::applicationAt(int index) const
255+{
256+ if (index >=0 && index < m_surfaceList.count()) {
257+ return m_surfaceList[index].application;
258+ } else {
259+ return nullptr;
260+ }
261+}
262+
263+int TopLevelSurfaceList::idAt(int index) const
264+{
265+ if (index >=0 && index < m_surfaceList.count()) {
266+ return m_surfaceList[index].id;
267+ } else {
268+ return 0;
269+ }
270+}
271+
272+int TopLevelSurfaceList::indexForId(int id) const
273+{
274+ for (int i = 0; i < m_surfaceList.count(); ++i) {
275+ if (m_surfaceList[i].id == id) {
276+ return i;
277+ }
278+ }
279+ return -1;
280+}
281+
282+void TopLevelSurfaceList::doRaiseId(int id)
283+{
284+ int fromIndex = indexForId(id);
285+ if (fromIndex != -1) {
286+ move(fromIndex, 0 /* toIndex */);
287+ }
288+}
289+
290+void TopLevelSurfaceList::raiseId(int id)
291+{
292+ if (m_modelState == IdleState) {
293+ DEBUG_MSG << "(id=" << id << ") - do it now.";
294+ doRaiseId(id);
295+ } else {
296+ DEBUG_MSG << "(id=" << id << ") - Model busy (modelState=" << m_modelState << "). Try again in the next event loop.";
297+ // The model has just signalled some change. If we have a Repeater responding to this update, it will get nuts
298+ // if we perform yet another model change straight away.
299+ //
300+ // A bad sympton of this problem is a Repeater.itemAt(index) call returning null event though Repeater.count says
301+ // the index is definitely within bounds.
302+ QMetaObject::invokeMethod(this, "raiseId", Qt::QueuedConnection, Q_ARG(int, id));
303+ }
304+}
305+
306+int TopLevelSurfaceList::generateId()
307+{
308+ int id = m_nextId;
309+ m_nextId = nextFreeId(m_nextId + 1);
310+ Q_EMIT nextIdChanged();
311+ return id;
312+}
313+
314+int TopLevelSurfaceList::nextFreeId(int candidateId)
315+{
316+ if (candidateId > m_maxId) {
317+ return nextFreeId(1);
318+ } else {
319+ if (indexForId(candidateId) == -1) {
320+ // it's indeed free
321+ return candidateId;
322+ } else {
323+ return nextFreeId(candidateId + 1);
324+ }
325+ }
326+}
327+
328+QString TopLevelSurfaceList::toString()
329+{
330+ QString str;
331+ for (int i = 0; i < m_surfaceList.count(); ++i) {
332+ auto item = m_surfaceList.at(i);
333+
334+ QString itemStr = QString("(index=%1,appId=%2,surface=0x%3,id=%4)")
335+ .arg(i)
336+ .arg(item.application->appId())
337+ .arg((qintptr)item.surface, 0, 16)
338+ .arg(item.id);
339+
340+ if (i > 0) {
341+ str.append(",");
342+ }
343+ str.append(itemStr);
344+ }
345+ return str;
346+}
347+
348+void TopLevelSurfaceList::addApplication(ApplicationInfoInterface *application)
349+{
350+ DEBUG_MSG << "(" << application->appId() << ")";
351+ Q_ASSERT(!m_applications.contains(application));
352+ m_applications.append(application);
353+
354+ MirSurfaceListInterface *surfaceList = application->surfaceList();
355+
356+ if (application->state() != ApplicationInfoInterface::Stopped) {
357+ if (surfaceList->count() == 0) {
358+ appendPlaceholder(application);
359+ } else {
360+ for (int i = 0; i < surfaceList->count(); ++i) {
361+ appendSurface(surfaceList->get(i), application);
362+ }
363+ }
364+ }
365+
366+ connect(surfaceList, &QAbstractItemModel::rowsInserted, this,
367+ [this, application, surfaceList](const QModelIndex & /*parent*/, int first, int last)
368+ {
369+ for (int i = last; i >= first; --i) {
370+ this->appendSurface(surfaceList->get(i), application);
371+ }
372+ });
373+}
374+
375+void TopLevelSurfaceList::removeApplication(ApplicationInfoInterface *application)
376+{
377+ DEBUG_MSG << "(" << application->appId() << ")";
378+ Q_ASSERT(m_applications.contains(application));
379+
380+ MirSurfaceListInterface *surfaceList = application->surfaceList();
381+
382+ disconnect(surfaceList, 0, this, 0);
383+
384+ Q_ASSERT(m_modelState == IdleState);
385+ m_modelState = RemovingState;
386+
387+ int i = 0;
388+ while (i < m_surfaceList.count()) {
389+ if (m_surfaceList.at(i).application == application) {
390+ beginRemoveRows(QModelIndex(), i, i);
391+ m_surfaceList.removeAt(i);
392+ endRemoveRows();
393+ Q_EMIT countChanged();
394+ Q_EMIT listChanged();
395+ } else {
396+ ++i;
397+ }
398+ }
399+
400+ m_modelState = IdleState;
401+
402+ DEBUG_MSG << " after " << toString();
403+
404+ m_applications.removeAll(application);
405+}
406+
407+QAbstractListModel *TopLevelSurfaceList::applicationsModel() const
408+{
409+ return m_applicationsModel;
410+}
411+
412+void TopLevelSurfaceList::setApplicationsModel(QAbstractListModel* value)
413+{
414+ if (m_applicationsModel == value) {
415+ return;
416+ }
417+
418+ DEBUG_MSG << "(" << value << ")";
419+
420+ Q_ASSERT(m_modelState == IdleState);
421+ m_modelState = ResettingState;
422+
423+ beginResetModel();
424+
425+ if (m_applicationsModel) {
426+ m_surfaceList.clear();
427+ m_applications.clear();
428+ disconnect(m_applicationsModel, 0, this, 0);
429+ }
430+
431+ m_applicationsModel = value;
432+
433+ if (m_applicationsModel) {
434+ findApplicationRole();
435+
436+ connect(m_applicationsModel, &QAbstractItemModel::rowsInserted,
437+ this, [this](const QModelIndex &/*parent*/, int first, int last) {
438+ for (int i = first; i <= last; ++i) {
439+ auto application = getApplicationFromModelAt(i);
440+ addApplication(application);
441+ }
442+ });
443+
444+ connect(m_applicationsModel, &QAbstractItemModel::rowsAboutToBeRemoved,
445+ this, [this](const QModelIndex &/*parent*/, int first, int last) {
446+ for (int i = first; i <= last; ++i) {
447+ auto application = getApplicationFromModelAt(i);
448+ removeApplication(application);
449+ }
450+ });
451+
452+ for (int i = 0; i < m_applicationsModel->rowCount(); ++i) {
453+ auto application = getApplicationFromModelAt(i);
454+ addApplication(application);
455+ }
456+ }
457+
458+ endResetModel();
459+ m_modelState = IdleState;
460+}
461+
462+ApplicationInfoInterface *TopLevelSurfaceList::getApplicationFromModelAt(int index)
463+{
464+ QModelIndex modelIndex = m_applicationsModel->index(index);
465+
466+ QVariant variant = m_applicationsModel->data(modelIndex, m_applicationRole);
467+
468+ // variant.value<ApplicationInfoInterface*>() returns null for some reason.
469+ return static_cast<ApplicationInfoInterface*>(variant.value<QObject*>());
470+}
471+
472+void TopLevelSurfaceList::findApplicationRole()
473+{
474+ QHash<int, QByteArray> namesHash = m_applicationsModel->roleNames();
475+
476+ m_applicationRole = -1;
477+ for (auto i = namesHash.begin(); i != namesHash.end() && m_applicationRole == -1; ++i) {
478+ if (i.value() == "application") {
479+ m_applicationRole = i.key();
480+ }
481+ }
482+
483+ if (m_applicationRole == -1) {
484+ qFatal("TopLevelSurfaceList: applicationsModel must have a \"application\" role.");
485+ }
486+}
plugins/WindowManager/TopLevelSurfaceList.hnew file+223
@@ -0,0 +1,223 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef TOPLEVELSURFACELIST_H
18+#define TOPLEVELSURFACELIST_H
19+
20+#include <QAbstractListModel>
21+#include <QList>
22+#include <QLoggingCategory>
23+
24+Q_DECLARE_LOGGING_CATEGORY(UNITY_TOPSURFACELIST)
25+
26+namespace unity {
27+ namespace shell {
28+ namespace application {
29+ class ApplicationInfoInterface;
30+ class MirSurfaceInterface;
31+ }
32+ }
33+}
34+
35+/**
36+ * @brief A model of top-level surfaces
37+ *
38+ * It's an abstraction of top-level application windows.
39+ *
40+ * When an entry first appears, it normaly doesn't have a surface yet, meaning that the application is
41+ * still starting up. A shell should then display a splash screen or saved screenshot of the application
42+ * until its surface comes up.
43+ *
44+ * As applications can have multiple surfaces and you can also have entries without surfaces at all,
45+ * the only way to unambiguously refer to an entry in this model is through its id.
46+ */
47+class TopLevelSurfaceList : public QAbstractListModel
48+{
49+
50+ Q_OBJECT
51+
52+ /**
53+ * @brief A list model of applications.
54+ *
55+ * It's expected to have a role called "application" which returns a ApplicationInfoInterface
56+ */
57+ Q_PROPERTY(QAbstractListModel* applicationsModel READ applicationsModel
58+ WRITE setApplicationsModel
59+ NOTIFY applicationsModelChanged)
60+
61+ /**
62+ * @brief Number of top-level surfaces in this model
63+ *
64+ * This is the same as rowCount, added in order to keep compatibility with QML ListModels.
65+ */
66+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
67+
68+ /**
69+ The id to be used on the next entry created
70+ Useful for tests
71+ */
72+ Q_PROPERTY(int nextId READ nextId NOTIFY nextIdChanged)
73+public:
74+
75+ /**
76+ * @brief The Roles supported by the model
77+ *
78+ * SurfaceRole - A MirSurfaceInterface. It will be null if the application is still starting up
79+ * ApplicationRole - An ApplicationInfoInterface
80+ * IdRole - A unique identifier for this entry. Useful to unambiguosly track elements as they move around in the list
81+ */
82+ enum Roles {
83+ SurfaceRole = Qt::UserRole,
84+ ApplicationRole = Qt::UserRole + 1,
85+ IdRole = Qt::UserRole + 2,
86+ };
87+
88+ explicit TopLevelSurfaceList(QObject *parent = nullptr);
89+ virtual ~TopLevelSurfaceList();
90+
91+ // QAbstractItemModel methods
92+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
93+ QVariant data(const QModelIndex& index, int role) const override;
94+ QHash<int, QByteArray> roleNames() const override {
95+ QHash<int, QByteArray> roleNames { {SurfaceRole, "surface"},
96+ {ApplicationRole, "application"},
97+ {IdRole, "id"} };
98+ return roleNames;
99+ }
100+
101+ int nextId() const { return m_nextId; }
102+
103+ QAbstractListModel *applicationsModel() const;
104+ void setApplicationsModel(QAbstractListModel*);
105+
106+public Q_SLOTS:
107+ /**
108+ * @brief Returns the surface at the given index
109+ *
110+ * It will be a nullptr if the application is still starting up and thus hasn't yet created
111+ * and drawn into a surface.
112+ */
113+ unity::shell::application::MirSurfaceInterface *surfaceAt(int index) const;
114+
115+ /**
116+ * @brief Returns the application at the given index
117+ */
118+ unity::shell::application::ApplicationInfoInterface *applicationAt(int index) const;
119+
120+ /**
121+ * @brief Returns the unique id of the element at the given index
122+ */
123+ int idAt(int index) const;
124+
125+ /**
126+ * @brief Returns the index where the row with the given id is located
127+ *
128+ * Returns -1 if there's no row with the given id.
129+ */
130+ int indexForId(int id) const;
131+
132+ /**
133+ * @brief Raises the row with the given id to index 0
134+ */
135+ void raiseId(int id);
136+
137+ void doRaiseId(int id);
138+
139+Q_SIGNALS:
140+ void countChanged();
141+
142+ /**
143+ * @brief Emitted when the list changes
144+ *
145+ * Emitted when model gains an element, loses an element or when elements exchange positions.
146+ */
147+ void listChanged();
148+
149+ void nextIdChanged();
150+
151+ void applicationsModelChanged();
152+
153+private:
154+ void addApplication(unity::shell::application::ApplicationInfoInterface *application);
155+ void removeApplication(unity::shell::application::ApplicationInfoInterface *application);
156+
157+ int indexOf(unity::shell::application::MirSurfaceInterface *surface);
158+ void raise(unity::shell::application::MirSurfaceInterface *surface);
159+ void move(int from, int to);
160+ void appendSurfaceHelper(unity::shell::application::MirSurfaceInterface *surface,
161+ unity::shell::application::ApplicationInfoInterface *application);
162+ void connectSurface(unity::shell::application::MirSurfaceInterface *surface);
163+ int generateId();
164+ int nextFreeId(int candidateId);
165+ QString toString();
166+ void onSurfaceDestroyed(unity::shell::application::MirSurfaceInterface *surface);
167+ void onSurfaceDied(unity::shell::application::MirSurfaceInterface *surface);
168+ void removeAt(int index);
169+ void findApplicationRole();
170+
171+ unity::shell::application::ApplicationInfoInterface *getApplicationFromModelAt(int index);
172+
173+ /*
174+ Placeholder for a future surface from a starting or running application.
175+ Enables shell to give immediate feedback to the user by showing, eg,
176+ a splash screen.
177+
178+ It's a model row containing a null surface and the given application.
179+ */
180+ void appendPlaceholder(unity::shell::application::ApplicationInfoInterface *application);
181+
182+ /*
183+ Adds a model row with the given surface and application
184+
185+ Alternatively, if a placeholder exists for the given application it's
186+ filled with the given surface instead.
187+ */
188+ void appendSurface(unity::shell::application::MirSurfaceInterface *surface,
189+ unity::shell::application::ApplicationInfoInterface *application);
190+
191+ struct ModelEntry {
192+ ModelEntry(unity::shell::application::MirSurfaceInterface *surface, unity::shell::application::ApplicationInfoInterface *application, int id)
193+ : surface(surface), application(application), id(id) {}
194+ unity::shell::application::MirSurfaceInterface *surface;
195+ unity::shell::application::ApplicationInfoInterface *application;
196+ int id;
197+ bool removeOnceSurfaceDestroyed{false};
198+ };
199+
200+ QList<ModelEntry> m_surfaceList;
201+ int m_nextId{1};
202+ static const int m_maxId{1000000};
203+
204+ // applications that are being monitored
205+ QList<unity::shell::application::ApplicationInfoInterface *> m_applications;
206+
207+ QAbstractListModel* m_applicationsModel{nullptr};
208+ int m_applicationRole{-1};
209+
210+ enum ModelState {
211+ IdleState,
212+ InsertingState,
213+ RemovingState,
214+ MovingState,
215+ ResettingState
216+ };
217+ ModelState m_modelState{IdleState};
218+};
219+
220+Q_DECLARE_METATYPE(TopLevelSurfaceList*)
221+Q_DECLARE_METATYPE(QAbstractListModel*)
222+
223+#endif // TOPLEVELSURFACELIST_H
plugins/WindowManager/WindowManagerPlugin.cppnew file+28
@@ -0,0 +1,28 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "WindowManagerPlugin.h"
18+
19+#include "TopLevelSurfaceList.h"
20+
21+#include <QtQml>
22+
23+void WindowManagerPlugin::registerTypes(const char *uri)
24+{
25+ qmlRegisterType<TopLevelSurfaceList>(uri, 0, 1, "TopLevelSurfaceList");
26+
27+ qRegisterMetaType<QAbstractListModel*>("QAbstractListModel*");
28+}
tests/mocks/Unity/Application/SessionManager.h → plugins/WindowManager/WindowManagerPlugin.hrenamed+10−22
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -14,31 +14,19 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef SESSIONMANAGER_H
18−#define SESSIONMANAGER_H
17+#ifndef WINDOWMANAGER_PLUGIN_H
18+#define WINDOWMANAGER_PLUGIN_H
1919
20−#include <QObject>
20+#include <QtQml/QQmlEngine>
21+#include <QtQml/QQmlExtensionPlugin>
2122
22−class Session;
23−
24−class SessionManager : public QObject
23+class WindowManagerPlugin : public QQmlExtensionPlugin
2524{
2625 Q_OBJECT
27−public:
28− explicit SessionManager(QObject *parent = 0);
29−
30− static SessionManager *singleton();
31−
32− Q_INVOKABLE Session* createSession(const QString& name, const QUrl& screenshot);
26+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
3327
34− void registerSession(Session *session);
35−
36−Q_SIGNALS:
37− void sessionStarting(Session *session);
38− void sessionStopping(Session *session);
39−
40−private:
41− static SessionManager *the_session_manager;
28+public:
29+ void registerTypes(const char *uri) override;
4230};
4331
44−#endif // SESSIONMANAGER_H
32+#endif // WINDOWMANAGER_PLUGIN_H
plugins/WindowManager/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module WindowManager
2+plugin windowmanager-qml
qml/Components/KeymapSwitcher.qml+1−9
@@ -57,16 +57,8 @@ QtObject {
5757 currentKeymapIndex = prevIndex;
5858 }
5959
60− // Code below will get much simpler with surface-based window management (the upcoming MirFocusController)
61− property var application: ApplicationManager.focusedApplicationId
62− ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
63− : null
64− property var session: application && application.session ? application.session : null
65− property var surface: session ? session.lastSurface : null
66−
6760 property Binding surfaceKeymapBinding: Binding {
68− target: root.surface
69− when: root.surface != null && root.surface.live
61+ target: MirFocusController.focusedSurface
7062 property: "keymap"
7163 value: root.currentKeymap
7264 }
qml/Greeter/Greeter.qml+5−4
@@ -64,7 +64,7 @@ Showable {
6464 d.selectUser(d.currentIndex, true);
6565 }
6666
67− function notifyAppFocused(appId) {
67+ function notifyAppFocusRequested(appId) {
6868 if (!active) {
6969 return;
7070 }
@@ -81,19 +81,20 @@ Showable {
8181 }
8282 }
8383
84− function notifyAboutToFocusApp(appId) {
84+ // Notify that the user has explicitly requested the given app through unity8 GUI.
85+ function notifyUserRequestedApp(appId) {
8586 if (!active) {
8687 return;
8788 }
8889
8990 // A hint that we're about to focus an app. This way we can look
9091 // a little more responsive, rather than waiting for the above
91− // notifyAppFocused call. We also need this in case we have a locked
92+ // notifyAppFocusRequested call. We also need this in case we have a locked
9293 // app, in order to show lockscreen instead of new app.
9394 d.startUnlock(false /* toTheRight */);
9495 }
9596
96− // This is a just a glorified notifyAboutToFocusApp(), but it does one
97+ // This is a just a glorified notifyUserRequestedApp(), but it does one
9798 // other thing: it hides any cover pages to the RIGHT, because the user
9899 // just came from a launcher drag starting on the left.
99100 // It also returns a boolean value, indicating whether there was a visual
qml/Launcher/LauncherPanel.qml+1
@@ -98,6 +98,7 @@ Rectangle {
9898 AbstractButton {
9999 id: dashItem
100100 anchors.fill: parent
101+ activeFocusOnPress: false
101102 onClicked: root.showDashHome()
102103 }
103104 Rectangle {
qml/Shell.qml+67−53
@@ -42,6 +42,7 @@ import Unity.Session 0.1
4242import Unity.DashCommunicator 0.1
4343import Unity.Indicators 0.1 as Indicators
4444import Cursor 1.0
45+import WindowManager 0.1
4546
4647
4748Item {
@@ -96,12 +97,44 @@ Item {
9697 }
9798 }
9899
99− // For autopilot consumption
100− readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
101−
102− // internal props from here onwards
103100 readonly property var mainApp:
104101 applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainApp : null
102+ onMainAppChanged: {
103+ if (mainApp) {
104+ _onMainAppChanged(mainApp.appId);
105+ }
106+ }
107+ Connections {
108+ target: ApplicationManager
109+ onFocusRequested: {
110+ if (shell.mainApp && shell.mainApp.appId === appId) {
111+ _onMainAppChanged(appId);
112+ }
113+ }
114+ }
115+ function _onMainAppChanged(appId) {
116+ if (wizard.active && appId != "" && appId != "unity8-dash") {
117+ // If this happens on first boot, we may be in edge
118+ // tutorial or wizard while receiving a call. But a call
119+ // is more important than wizard so just bail out of those.
120+ tutorial.finish();
121+ wizard.hide();
122+ }
123+
124+ if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
125+ // If we are in the middle of a call, make dialer lockedApp and show it.
126+ // This can happen if user backs out of dialer back to greeter, then
127+ // launches dialer again.
128+ greeter.lockedApp = appId;
129+ }
130+ greeter.notifyAppFocusRequested(appId);
131+
132+ panel.indicators.hide();
133+ launcher.hide();
134+ }
135+
136+ // For autopilot consumption
137+ readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
105138
106139 // Disable everything while greeter is waiting, so that the user can't swipe
107140 // the greeter or launcher until we know whether the session is locked.
@@ -139,9 +172,6 @@ Item {
139172
140173 Component.onCompleted: {
141174 theme.name = "Ubuntu.Components.Themes.SuruDark"
142− if (ApplicationManager.count > 0) {
143− ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
144− }
145175 finishStartUpTimer.start();
146176 }
147177
@@ -198,36 +228,10 @@ Item {
198228 width: parent.width
199229 height: parent.height
200230
201− Connections {
202− target: ApplicationManager
203−
204− // This signal is also fired when we try to focus the current app
205− // again. We rely on this!
206− onFocusedApplicationIdChanged: {
207− var appId = ApplicationManager.focusedApplicationId;
208−
209− if (wizard.active && appId != "" && appId != "unity8-dash") {
210− // If this happens on first boot, we may be in edge
211− // tutorial or wizard while receiving a call. But a call
212− // is more important than wizard so just bail out of those.
213− tutorial.finish();
214− wizard.hide();
215− }
216−
217− if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
218− // If we are in the middle of a call, make dialer lockedApp and show it.
219− // This can happen if user backs out of dialer back to greeter, then
220− // launches dialer again.
221− greeter.lockedApp = appId;
222− }
223− greeter.notifyAppFocused(appId);
224−
225− panel.indicators.hide();
226− }
227−
228− onApplicationAdded: {
229− launcher.hide();
230− }
231+ TopLevelSurfaceList {
232+ id: topLevelSurfaceList
233+ objectName: "topLevelSurfaceList"
234+ applicationsModel: ApplicationManager
231235 }
232236
233237 Loader {
@@ -258,6 +262,10 @@ Item {
258262 return "Stages/DesktopStage.qml";
259263 }
260264 }
265+ // TODO: Ensure the current stage is destroyed before the new one gets loaded.
266+ // Currently the new one will get loaded while the old is still hanging
267+ // around for a bit, which might lead to conflicts where both stages
268+ // change the model simultaneously.
261269 onQmlComponentChanged: {
262270 if (item) item.stageAboutToBeUnloaded();
263271 source = qmlComponent;
@@ -270,6 +278,11 @@ Item {
270278
271279 onInteractiveChanged: { if (interactive) { focus = true; } }
272280
281+ Binding {
282+ target: applicationsDisplayLoader.item
283+ property: "focus"
284+ value: true
285+ }
273286 Binding {
274287 target: applicationsDisplayLoader.item
275288 property: "objectName"
@@ -356,6 +369,16 @@ Item {
356369 property: "leftMargin"
357370 value: shell.usageScenario == "desktop" && !settings.autohideLauncher ? launcher.panelWidth: 0
358371 }
372+ Binding {
373+ target: applicationsDisplayLoader.item
374+ property: "applicationManager"
375+ value: ApplicationManager
376+ }
377+ Binding {
378+ target: applicationsDisplayLoader.item
379+ property: "topLevelSurfaceList"
380+ value: topLevelSurfaceList
381+ }
359382 }
360383 }
361384
@@ -370,16 +393,6 @@ Item {
370393 z: notifications.useModal || panel.indicators.shown || wizard.active || tutorial.running ? overlay.z + 1 : overlay.z - 1
371394 }
372395
373− Connections {
374− target: SessionManager
375− onSessionStopping: {
376− if (!session.parentSession && !session.application) {
377− // nothing is using it. delete it right away
378− session.release();
379− }
380− }
381− }
382−
383396 Loader {
384397 id: greeterLoader
385398 anchors.fill: parent
@@ -471,7 +484,7 @@ Item {
471484 }
472485
473486 function showHome() {
474− greeter.notifyAboutToFocusApp("unity8-dash");
487+ greeter.notifyUserRequestedApp("unity8-dash");
475488
476489 var animate = !lightDM.greeter.active && !stages.shown
477490 dash.setCurrentScope(0, animate, false)
@@ -520,9 +533,10 @@ Item {
520533 greeterShown: greeter.shown
521534 }
522535
523− readonly property bool topmostApplicationIsFullscreen: mainApp && mainApp.fullscreen
524−
525− fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
536+ readonly property bool focusedSurfaceIsFullscreen: MirFocusController.focusedSurface
537+ ? MirFocusController.focusedSurface.state === Mir.FullscreenState
538+ : false
539+ fullscreenMode: (focusedSurfaceIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
526540 || greeter.hasLockedApp
527541 locked: greeter && greeter.active
528542 }
@@ -555,7 +569,7 @@ Item {
555569 }
556570 }
557571 onLauncherApplicationSelected: {
558− greeter.notifyAboutToFocusApp(appId);
572+ greeter.notifyUserRequestedApp(appId);
559573 shell.activateApplication(appId);
560574 }
561575 onShownChanged: {
@@ -719,7 +733,7 @@ Item {
719733 onMouseMoved: { cursor.opacity = 1; }
720734 }
721735
722− // non-visual item
736+ // non-visual object
723737 KeymapSwitcher {}
724738
725739 Rectangle {
qml/Stages/AbstractStage.qml+9−2
@@ -18,12 +18,19 @@ import QtQuick 2.4
1818import Ubuntu.Components 1.3
1919import GSettings 1.0
2020
21−Rectangle {
21+FocusScope {
2222 id: root
2323
24− color: "#060606"
24+ property alias color: backRect.color
25+ Rectangle {
26+ id: backRect
27+ color: "#060606"
28+ anchors.fill: parent
29+ }
2530
2631 // Controls to be set from outside
32+ property QtObject applicationManager
33+ property QtObject topLevelSurfaceList
2734 property bool altTabPressed
2835 property url background
2936 property bool beingResized
qml/Stages/Animations/BaseSessionAnimation.qmldeleted−96
@@ -1,96 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−
19−/* This is the base case for surface animations, used when adding/removing * child surfaces.
20− * The class is meant to be overridden and changes/animations provided for state changes.
21− * NB. It is important to release the surface at the end of the "out" animation.
22− *
23− * Example - Simple fade in/out
24− *
25− * BaseSurfaceAnimation {
26− * outChanges: [ PropertyChanges { target: animation.surface; opacity: 0.0 } ]
27− * outAnimations: [
28− SequentialAnimation {
29− * NumberAnimation { target: animation.surface; property: "opacity"; duration: 300 }
30− * ScriptAction { script: { if (animation.parent.removing) animation.surface.release(); } }
31− * }
32− * ]
33− *
34− * inChanges: [ PropertyChanges { target: animation.surface; opacity: 1.0 } ]
35− * inAnimations: [ NumberAnimation { target: animation.surface; property: "opacity"; duration: 300 } ]
36− * }
37− */
38−Item {
39− id: base
40− property var container
41− objectName: "sessionAnimation"
42−
43− // changes applied when state changes to "from"
44− property list<QtObject> fromChanges
45− // transition animations when changing state to "from"
46− property list<QtObject> fromAnimations
47−
48− // changes applied when state changes to "to"
49− property list<QtObject> toChanges
50− // transition animations when changing state to "to"
51− property list<QtObject> toAnimations
52−
53− function start() {
54− // "prep" state forces toChanges without transition animations.
55− state = "prep"
56− state = "to";
57− }
58− function end() {
59− state = "from";
60− }
61−
62− signal completed()
63−
64− states: [
65− State {
66− name: "baseAnimation"
67− PropertyChanges { target: container; anchors.fill: undefined }
68− },
69−
70− State {
71− name: "prep"
72− extend: "baseAnimation"
73− changes: fromChanges
74− },
75− State {
76− name: "from"
77− extend: "prep"
78− },
79− State {
80− name: "in"
81− extend: "baseAnimation"
82− changes: toChanges
83− }
84− ]
85−
86− transitions: [
87− Transition {
88− to: "from"
89− animations: fromAnimations
90− },
91− Transition {
92− to: "to"
93− animations: toAnimations
94− }
95− ]
96−}
qml/Stages/Animations/SwipeFromBottomAnimation.qmldeleted−53
@@ -1,53 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import Ubuntu.Components 1.3
19−
20−BaseSessionAnimation {
21− id: animation
22−
23− fromChanges: [
24− AnchorChanges {
25− target: container;
26− anchors.top: container.parent.bottom
27− }
28− ]
29− fromAnimations: [
30− SequentialAnimation {
31− // clip so we don't go out of parent's bounds during spread
32− PropertyAction { target: container.parent; property: "clip"; value: true }
33− AnchorAnimation { easing: UbuntuAnimation.StandardEasing; duration: UbuntuAnimation.BriskDuration }
34− PropertyAction { target: container.parent; property: "clip"; value: false }
35− ScriptAction { script: { animation.completed(); } }
36− }
37− ]
38−
39− toChanges: [
40− AnchorChanges {
41− target: container;
42− anchors.top: container.parent.top
43− }
44− ]
45− toAnimations: [
46− SequentialAnimation {
47− // clip so we don't go out of parent's bounds during spread
48− PropertyAction { target: container.parent; property: "clip"; value: true }
49− AnchorAnimation { easing: UbuntuAnimation.StandardEasing; duration: UbuntuAnimation.BriskDuration }
50− PropertyAction { target: container.parent; property: "clip"; value: false }
51− }
52− ]
53−}
qml/Stages/ApplicationWindow.qml+110−68
@@ -20,35 +20,77 @@ import Unity.Application 0.1
2020
2121FocusScope {
2222 id: root
23− implicitWidth: sessionContainer.implicitWidth
24− implicitHeight: sessionContainer.implicitHeight
23+ implicitWidth: surfaceContainer.implicitWidth
24+ implicitHeight: surfaceContainer.implicitHeight
2525
2626 // to be read from outside
27− property alias interactive: sessionContainer.interactive
27+ property alias interactive: surfaceContainer.interactive
2828 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
29− readonly property string title: sessionContainer.surface && sessionContainer.surface.name !== "" ?
30− sessionContainer.surface.name : d.name
29+ readonly property string title: surface && surface.name !== "" ? surface.name : d.name
3130
3231 // overridable from outside
33− property bool fullscreen: application ? application.fullscreen : false
32+ property bool fullscreen: {
33+ if (surface) {
34+ return surface.state === Mir.FullscreenState;
35+ } else if (application) {
36+ return application.fullscreen;
37+ } else {
38+ return false;
39+ }
40+ }
3441
3542 // to be set from outside
43+ property QtObject surface
3644 property QtObject application
3745 property int surfaceOrientationAngle
38− property alias resizeSurface: sessionContainer.resizeSurface
46+ property alias resizeSurface: surfaceContainer.resizeSurface
3947 property int requestedWidth: -1
4048 property int requestedHeight: -1
4149
42− readonly property int minimumWidth: sessionContainer.surface ? sessionContainer.surface.minimumWidth : 0
43− readonly property int minimumHeight: sessionContainer.surface ? sessionContainer.surface.minimumHeight : 0
44− readonly property int maximumWidth: sessionContainer.surface ? sessionContainer.surface.maximumWidth : 0
45− readonly property int maximumHeight: sessionContainer.surface ? sessionContainer.surface.maximumHeight : 0
46− readonly property int widthIncrement: sessionContainer.surface ? sessionContainer.surface.widthIncrement : 0
47− readonly property int heightIncrement: sessionContainer.surface ? sessionContainer.surface.heightIncrement : 0
50+ readonly property int minimumWidth: surface ? surface.minimumWidth : 0
51+ readonly property int minimumHeight: surface ? surface.minimumHeight : 0
52+ readonly property int maximumWidth: surface ? surface.maximumWidth : 0
53+ readonly property int maximumHeight: surface ? surface.maximumHeight : 0
54+ readonly property int widthIncrement: surface ? surface.widthIncrement : 0
55+ readonly property int heightIncrement: surface ? surface.heightIncrement : 0
56+
57+ onSurfaceChanged: {
58+ // The order in which the instructions are executed here matters, to that the correct state
59+ // transitions in stateGroup take place.
60+ // More specifically, the moment surfaceContainer.surface gets updated relative to the
61+ // other instructions.
62+ if (surface) {
63+ surfaceContainer.surface = surface;
64+ d.liveSurface = surface.live;
65+ d.hadSurface = false;
66+ surfaceInitTimer.start();
67+ } else {
68+ if (d.surfaceInitialized) {
69+ d.hadSurface = true;
70+ }
71+ d.surfaceInitialized = false;
72+ surfaceContainer.surface = null;
73+ }
74+ }
4875
4976 QtObject {
5077 id: d
5178
79+ property bool liveSurface: false;
80+ property var con: Connections {
81+ target: root.surface
82+ onLiveChanged: d.liveSurface = root.surface.live
83+ }
84+ // using liveSurface instead of root.surface.live because with the latter
85+ // this expression is not reevaluated when root.surface changes
86+ readonly property bool needToTakeScreenshot: root.surface && d.surfaceInitialized && !d.liveSurface
87+ && applicationState !== ApplicationInfoInterface.Running
88+ onNeedToTakeScreenshotChanged: {
89+ if (needToTakeScreenshot && screenshotImage.status === Image.Null) {
90+ screenshotImage.take();
91+ }
92+ }
93+
5294 // helpers so that we don't have to check for the existence of an application everywhere
5395 // (in order to avoid breaking qml binding due to a javascript exception)
5496 readonly property string name: root.application ? root.application.name : ""
@@ -62,17 +104,7 @@ FocusScope {
62104 readonly property color splashColorFooter: root.application ? root.application.splashColorFooter : "#00000000"
63105
64106 // Whether the Application had a surface before but lost it.
65− property bool hadSurface: sessionContainer.surfaceContainer.hadSurface
66−
67− readonly property bool needToTakeScreenshot:
68− ((sessionContainer.surface && d.surfaceInitialized) || d.hadSurface)
69− && screenshotImage.status === Image.Null
70− && d.applicationState === ApplicationInfoInterface.Stopped
71− onNeedToTakeScreenshotChanged: {
72− if (needToTakeScreenshot) {
73− screenshotImage.take();
74− }
75− }
107+ property bool hadSurface: false
76108
77109 //FIXME - this is a hack to avoid the first few rendered frames as they
78110 // might show the UI accommodating due to surface resizes on startup.
@@ -99,7 +131,9 @@ FocusScope {
99131 Timer {
100132 id: surfaceInitTimer
101133 interval: 100
102− onTriggered: { if (sessionContainer.surface) {d.surfaceInitialized = true;} }
134+ onTriggered: {
135+ if (root.surface && root.surface.live) {d.surfaceInitialized = true;}
136+ }
103137 }
104138
105139 Timer {
@@ -117,7 +151,7 @@ FocusScope {
117151 function take() {
118152 // Save memory by using a half-resolution (thus quarter size) screenshot.
119153 // Do not make this a binding, we can only take the screenshot once!
120− sessionContainer.grabToImage(
154+ surfaceContainer.grabToImage(
121155 function(result) {
122156 screenshotImage.source = result.url;
123157 },
@@ -144,46 +178,33 @@ FocusScope {
144178 }
145179 }
146180
147− SessionContainer {
148− id: sessionContainer
149− // A fake application might not even have a session property.
150− session: application && application.session ? application.session : null
151−
181+ SurfaceContainer {
182+ id: surfaceContainer
152183 requestedWidth: root.requestedWidth
153184 requestedHeight: root.requestedHeight
154−
155185 surfaceOrientationAngle: application && application.rotatesWindowContents ? root.surfaceOrientationAngle : 0
156−
157− onSurfaceChanged: {
158− if (sessionContainer.surface) {
159− surfaceInitTimer.start();
160− } else {
161− d.surfaceInitialized = false;
162− }
163− }
164−
165186 focus: true
166187 }
167188
168− // SessionContainer size drives ApplicationWindow size
189+ // SurfaceContainer size drives ApplicationWindow size
169190 Binding {
170191 target: root; property: "width"
171− value: stateGroup.state === "surface" ? sessionContainer.width : root.requestedWidth
192+ value: stateGroup.state === "surface" ? surfaceContainer.width : root.requestedWidth
172193 when: root.requestedWidth >= 0
173194 }
174195 Binding {
175196 target: root; property: "height"
176− value: stateGroup.state === "surface" ? sessionContainer.height : root.requestedHeight
197+ value: stateGroup.state === "surface" ? surfaceContainer.height : root.requestedHeight
177198 when: root.requestedHeight >= 0
178199 }
179200
180− // ApplicationWindow size drives SessionContainer size
201+ // ApplicationWindow size drives SurfaceContainer size
181202 Binding {
182− target: sessionContainer; property: "width"; value: root.width
203+ target: surfaceContainer; property: "width"; value: root.width
183204 when: root.requestedWidth < 0
184205 }
185206 Binding {
186− target: sessionContainer; property: "height"; value: root.height
207+ target: surfaceContainer; property: "height"; value: root.height
187208 when: root.requestedHeight < 0
188209 }
189210
@@ -194,32 +215,43 @@ FocusScope {
194215 State {
195216 name: "void"
196217 when:
197− d.hadSurface && (!sessionContainer.surface || !d.surfaceInitialized)
218+ d.hadSurface && (!root.surface || !d.surfaceInitialized)
198219 &&
199220 screenshotImage.status !== Image.Ready
200221 },
201222 State {
202223 name: "splashScreen"
203224 when:
204− !d.hadSurface && (!sessionContainer.surface || !d.surfaceInitialized)
225+ !d.hadSurface && (!root.surface || !d.surfaceInitialized)
205226 &&
206227 screenshotImage.status !== Image.Ready
207228 },
208229 State {
209230 name: "surface"
210231 when:
211− (sessionContainer.surface && d.surfaceInitialized)
232+ (root.surface && d.surfaceInitialized)
212233 &&
213− (d.applicationState !== ApplicationInfoInterface.Stopped
214− || screenshotImage.status !== Image.Ready)
234+ (d.liveSurface ||
235+ (d.applicationState !== ApplicationInfoInterface.Running
236+ && screenshotImage.status !== Image.Ready))
215237 },
216238 State {
217239 name: "screenshot"
218240 when:
219241 screenshotImage.status === Image.Ready
220242 &&
221− (d.applicationState === ApplicationInfoInterface.Stopped
222− || !sessionContainer.surface || !d.surfaceInitialized)
243+ (d.applicationState !== ApplicationInfoInterface.Running
244+ || !root.surface || !d.surfaceInitialized)
245+ },
246+ State {
247+ // This is a dead end. From here we expect the surface to be removed from the model
248+ // shortly after we stop referencing to it in our SurfaceContainer.
249+ name: "closed"
250+ when:
251+ // The surface died while the application is running. It must have been closed
252+ // by the shell or the application decided to destroy it by itself
253+ root.surface && d.surfaceInitialized && !d.liveSurface
254+ && d.applicationState === ApplicationInfoInterface.Running
223255 }
224256 ]
225257
@@ -227,17 +259,17 @@ FocusScope {
227259 Transition {
228260 from: ""; to: "splashScreen"
229261 PropertyAction { target: splashLoader; property: "active"; value: true }
230− PropertyAction { target: sessionContainer.surfaceContainer
262+ PropertyAction { target: surfaceContainer
231263 property: "visible"; value: false }
232264 },
233265 Transition {
234266 from: "splashScreen"; to: "surface"
235267 SequentialAnimation {
236− PropertyAction { target: sessionContainer.surfaceContainer
268+ PropertyAction { target: surfaceContainer
237269 property: "opacity"; value: 0.0 }
238− PropertyAction { target: sessionContainer.surfaceContainer
270+ PropertyAction { target: surfaceContainer
239271 property: "visible"; value: true }
240− UbuntuNumberAnimation { target: sessionContainer.surfaceContainer; property: "opacity";
272+ UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
241273 from: 0.0; to: 1.0
242274 duration: UbuntuAnimation.BriskDuration }
243275 ScriptAction { script: {
@@ -253,12 +285,12 @@ FocusScope {
253285 surfaceIsOldTimer.stop();
254286 d.surfaceOldEnoughToBeResized = false;
255287 splashLoader.active = true;
256− sessionContainer.surfaceContainer.visible = true;
288+ surfaceContainer.visible = true;
257289 } }
258290 UbuntuNumberAnimation { target: splashLoader; property: "opacity";
259291 from: 0.0; to: 1.0
260292 duration: UbuntuAnimation.BriskDuration }
261− PropertyAction { target: sessionContainer.surfaceContainer
293+ PropertyAction { target: surfaceContainer
262294 property: "visible"; value: false }
263295 }
264296 },
@@ -274,15 +306,16 @@ FocusScope {
274306 from: 0.0; to: 1.0
275307 duration: UbuntuAnimation.BriskDuration }
276308 ScriptAction { script: {
277− sessionContainer.surfaceContainer.visible = false;
278− if (sessionContainer.session) { sessionContainer.session.release(); }
309+ surfaceContainer.visible = false;
310+ surfaceContainer.surface = null;
311+ d.hadSurface = true;
279312 } }
280313 }
281314 },
282315 Transition {
283316 from: "screenshot"; to: "surface"
284317 SequentialAnimation {
285− PropertyAction { target: sessionContainer.surfaceContainer
318+ PropertyAction { target: surfaceContainer
286319 property: "visible"; value: true }
287320 UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
288321 from: 1.0; to: 0.0
@@ -310,22 +343,31 @@ FocusScope {
310343 ScriptAction { script: {
311344 surfaceIsOldTimer.stop();
312345 d.surfaceOldEnoughToBeResized = false;
313− sessionContainer.surfaceContainer.visible = false;
314− if (sessionContainer.session) { sessionContainer.session.release(); }
346+ surfaceContainer.visible = false;
315347 } }
316348 },
317349 Transition {
318350 from: "void"; to: "surface"
319351 SequentialAnimation {
320− PropertyAction { target: sessionContainer.surfaceContainer; property: "opacity"; value: 0.0 }
321− PropertyAction { target: sessionContainer.surfaceContainer; property: "visible"; value: true }
322− UbuntuNumberAnimation { target: sessionContainer.surfaceContainer; property: "opacity";
352+ PropertyAction { target: surfaceContainer; property: "opacity"; value: 0.0 }
353+ PropertyAction { target: surfaceContainer; property: "visible"; value: true }
354+ UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
323355 from: 0.0; to: 1.0
324356 duration: UbuntuAnimation.BriskDuration }
325357 ScriptAction { script: {
326358 surfaceIsOldTimer.start();
327359 } }
328360 }
361+ },
362+ Transition {
363+ to: "closed"
364+ SequentialAnimation {
365+ ScriptAction { script: {
366+ surfaceContainer.visible = false;
367+ surfaceContainer.surface = null;
368+ d.hadSurface = true;
369+ } }
370+ }
329371 }
330372 ]
331373 }
qml/Stages/DecoratedWindow.qml+5−5
@@ -26,10 +26,10 @@ FocusScope {
2626 width: applicationWindow.width
2727 height: (decorationShown ? decoration.height : 0) + applicationWindow.height
2828
29− property alias window: applicationWindow
3029 property alias application: applicationWindow.application
30+ property alias surface: applicationWindow.surface
3131 property alias active: decoration.active
32− property alias title: decoration.title
32+ readonly property alias title: applicationWindow.title
3333 property alias fullscreen: applicationWindow.fullscreen
3434
3535 readonly property bool decorationShown: !fullscreen
@@ -79,11 +79,11 @@ FocusScope {
7979 WindowDecoration {
8080 id: decoration
8181 target: root.parent
82− objectName: application ? "appWindowDecoration_" + application.appId : "appWindowDecoration_null"
82+ objectName: "appWindowDecoration"
8383 anchors { left: parent.left; top: parent.top; right: parent.right }
8484 height: units.gu(3)
8585 width: root.width
86− title: window.title
86+ title: applicationWindow.title
8787 visible: root.decorationShown
8888
8989 onClose: root.close();
@@ -94,7 +94,7 @@ FocusScope {
9494
9595 ApplicationWindow {
9696 id: applicationWindow
97− objectName: application ? "appWindow_" + application.appId : "appWindow_null"
97+ objectName: "appWindow"
9898 anchors.top: parent.top
9999 anchors.topMargin: decoration.height
100100 anchors.left: parent.left
qml/Stages/DesktopSpread.qml+22−18
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
2020import Ubuntu.Gestures 0.1
2121import Unity.Application 0.1
2222import "../Components"
23+import Utils 0.1
2324
2425FocusScope {
2526 id: root
@@ -69,12 +70,12 @@ FocusScope {
6970 }
7071
7172 function selectNext(isAutoRepeat) {
72− if (isAutoRepeat && spreadRepeater.highlightedIndex >= ApplicationManager.count -1) {
73+ if (isAutoRepeat && spreadRepeater.highlightedIndex >= topLevelSurfaceList.count -1) {
7374 return; // AutoRepeat is not allowed to wrap around
7475 }
7576
76− spreadRepeater.highlightedIndex = (spreadRepeater.highlightedIndex + 1) % ApplicationManager.count;
77− var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, spreadRepeater.highlightedIndex - 3));
77+ spreadRepeater.highlightedIndex = (spreadRepeater.highlightedIndex + 1) % topLevelSurfaceList.count;
78+ var newContentX = ((spreadFlickable.contentWidth) / (topLevelSurfaceList.count + 1)) * Math.max(0, Math.min(topLevelSurfaceList.count - 5, spreadRepeater.highlightedIndex - 3));
7879 if (spreadFlickable.contentX < newContentX || spreadRepeater.highlightedIndex == 0) {
7980 spreadFlickable.snapTo(newContentX)
8081 }
@@ -85,10 +86,10 @@ FocusScope {
8586 return; // AutoRepeat is not allowed to wrap around
8687 }
8788
88− var newIndex = spreadRepeater.highlightedIndex - 1 >= 0 ? spreadRepeater.highlightedIndex - 1 : ApplicationManager.count - 1;
89+ var newIndex = spreadRepeater.highlightedIndex - 1 >= 0 ? spreadRepeater.highlightedIndex - 1 : topLevelSurfaceList.count - 1;
8990 spreadRepeater.highlightedIndex = newIndex;
90− var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, spreadRepeater.highlightedIndex - 1));
91− if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
91+ var newContentX = ((spreadFlickable.contentWidth) / (topLevelSurfaceList.count + 1)) * Math.max(0, Math.min(topLevelSurfaceList.count - 5, spreadRepeater.highlightedIndex - 1));
92+ if (spreadFlickable.contentX > newContentX || newIndex == topLevelSurfaceList.count -1) {
9293 spreadFlickable.snapTo(newContentX)
9394 }
9495 }
@@ -98,8 +99,8 @@ FocusScope {
9899 if (spreadContainer.visible) {
99100 root.playFocusAnimation(spreadRepeater.highlightedIndex)
100101 }
101− var application = ApplicationManager.get(spreadRepeater.highlightedIndex);
102− ApplicationManager.requestFocusApplication(application.appId);
102+ var surface = topLevelSurfaceList.surfaceAt(spreadRepeater.highlightedIndex);
103+ surface.requestFocus();
103104 }
104105 }
105106
@@ -144,7 +145,7 @@ FocusScope {
144145 Repeater {
145146 id: spreadRepeater
146147 objectName: "spreadRepeater"
147− model: ApplicationManager
148+ model: topLevelSurfaceList
148149
149150 property int highlightedIndex: -1
150151 property int closingIndex: -1
@@ -182,7 +183,8 @@ FocusScope {
182183 objectName: "clippedSpreadDelegate"
183184 anchors.left: parent.left
184185 anchors.top: parent.top
185− application: ApplicationManager.get(index)
186+ application: model.application
187+ surface: model.surface
186188 width: spreadMaths.spreadHeight
187189 height: spreadMaths.spreadHeight
188190
@@ -216,7 +218,7 @@ FocusScope {
216218 id: spreadMaths
217219 flickable: spreadFlickable
218220 itemIndex: index
219− totalItems: Math.max(6, ApplicationManager.count)
221+ totalItems: Math.max(6, topLevelSurfaceList.count)
220222 sceneHeight: root.height
221223 itemHeight: spreadDelegate.height
222224 }
@@ -310,7 +312,7 @@ FocusScope {
310312 Layout.preferredWidth: height * 8 / 7.6
311313 image: Image {
312314 anchors.fill: parent
313− source: model.icon
315+ source: model.application.icon
314316 Rectangle {
315317 anchors.fill: parent
316318 color: "black"
@@ -324,7 +326,9 @@ FocusScope {
324326 Label {
325327 Layout.fillWidth: true
326328 Layout.preferredHeight: units.gu(6)
327− text: model.name
329+ property string surfaceName: model.surface ? model.surface.name : ""
330+ property string applicationName: model.application ? model.application.name : ""
331+ text: surfaceName ? surfaceName : applicationName
328332 wrapMode: Text.WordWrap
329333 elide: Text.ElideRight
330334 maximumLineCount: 2
@@ -354,7 +358,7 @@ FocusScope {
354358 anchors.margins: -units.gu(2)
355359 onClicked: {
356360 spreadRepeater.closingIndex = index;
357− ApplicationManager.stopApplication(model.appId)
361+ model.surface.close();
358362 }
359363 }
360364 }
@@ -421,7 +425,7 @@ FocusScope {
421425 objectName: "spreadFlickable"
422426 anchors.fill: parent
423427 property int minContentWidth: 6 * Math.min(height / 4, width / 5)
424− contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
428+ contentWidth: Math.max(6, topLevelSurfaceList.count) * Math.min(height / 4, width / 5)
425429 enabled: false
426430
427431 function snapTo(contentX) {
@@ -523,7 +527,7 @@ FocusScope {
523527 Label {
524528 id: currentSelectedLabel
525529 anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
526− text: spreadRepeater.highlightedIndex >= 0 ? ApplicationManager.get(spreadRepeater.highlightedIndex).name : ""
530+ text: spreadRepeater.highlightedIndex >= 0 ? topLevelSurfaceList.surfaceAt(spreadRepeater.highlightedIndex).name : ""
527531 visible: false
528532 fontSize: "large"
529533 }
@@ -545,7 +549,7 @@ FocusScope {
545549 from: "*"
546550 to: "altTab"
547551 SequentialAnimation {
548− PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
552+ PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(topLevelSurfaceList.count - 1, 1) }
549553 PauseAnimation { duration: spreadContainer.animateIn ? 0 : 140 }
550554 PropertyAction { target: workspaceSelector; property: "visible" }
551555 PropertyAction { target: spreadContainer; property: "visible" }
qml/Stages/DesktopSpreadDelegate.qml+4−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -25,12 +25,13 @@ Item {
2525
2626 property alias window: applicationWindow
2727 property alias application: applicationWindow.application
28+ property alias surface: applicationWindow.surface
2829
2930 property bool highlightShown: false
3031 property real shadowOpacity: 1
3132
32− property int windowWidth: application && application.session && application.session.lastSurface ? application.session.lastSurface.size.width : 0
33− property int windowHeight: application && application.session && application.session.lastSurface ? application.session.lastSurface.size.height : 0
33+ property int windowWidth: surface ? surface.size.width : 0
34+ property int windowHeight: surface ? surface.size.height : 0
3435
3536 state: "normal"
3637 states: [
qml/Stages/DesktopStage.qml+159−98
@@ -39,45 +39,18 @@ AbstractStage {
3939 // Used by TutorialRight
4040 property bool spreadShown: spread.state == "altTab"
4141
42− mainApp: ApplicationManager.focusedApplicationId
43− ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
44− : null
42+ mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
4543
4644 // application windows never rotate independently
4745 mainAppWindowOrientationAngle: shellOrientationAngle
4846
4947 orientationChangesEnabled: true
5048
51− Connections {
52− target: ApplicationManager
53− onApplicationAdded: {
54− if (spread.state == "altTab") {
55− spread.state = "";
56− }
57−
58− ApplicationManager.focusApplication(appId);
59− }
60−
61− onApplicationRemoved: {
62− priv.focusNext();
63− }
64−
65− onFocusRequested: {
66− var appIndex = priv.indexOf(appId);
67− var appDelegate = appRepeater.itemAt(appIndex);
68− appDelegate.restore();
69−
70− if (spread.state == "altTab") {
71− spread.cancel();
72− }
73− }
74− }
75−
7649 GlobalShortcut {
7750 id: closeWindowShortcut
7851 shortcut: Qt.AltModifier|Qt.Key_F4
79− onTriggered: ApplicationManager.stopApplication(priv.focusedAppId)
80− active: priv.focusedAppId !== ""
52+ onTriggered: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
53+ active: priv.focusedAppDelegate !== null
8154 }
8255
8356 GlobalShortcut {
@@ -121,40 +94,40 @@ AbstractStage {
12194 active: priv.focusedAppDelegate !== null
12295 }
12396
97+ Connections {
98+ target: root.topLevelSurfaceList
99+ onCountChanged: {
100+ if (spread.state == "altTab") {
101+ spread.cancel();
102+ }
103+ }
104+ }
105+
124106 QtObject {
125107 id: priv
108+ objectName: "DesktopStagePrivate"
126109
127− readonly property string focusedAppId: ApplicationManager.focusedApplicationId
128− readonly property var focusedAppDelegate: {
129− var index = indexOf(focusedAppId);
130− return index >= 0 && index < appRepeater.count ? appRepeater.itemAt(index) : null
110+ property var focusedAppDelegate: null
111+ onFocusedAppDelegateChanged: {
112+ if (spread.state == "altTab") {
113+ spread.state = "";
114+ }
131115 }
132− onFocusedAppDelegateChanged: updateForegroundMaximizedApp();
133116
134− property int foregroundMaximizedAppZ: -1
135− property int foregroundMaximizedAppIndex: -1 // for stuff like drop shadow and focusing maximized app by clicking panel
117+ property var foregroundMaximizedAppDelegate: null // for stuff like drop shadow and focusing maximized app by clicking panel
136118
137119 function updateForegroundMaximizedApp() {
138− var tmp = -1;
139− var tmpAppId = -1;
140− for (var i = appRepeater.count - 1; i >= 0; i--) {
120+ var found = false;
121+ for (var i = 0; i < appRepeater.count && !found; i++) {
141122 var item = appRepeater.itemAt(i);
142123 if (item && item.visuallyMaximized) {
143− tmpAppId = i;
144− tmp = Math.max(tmp, item.normalZ);
124+ foregroundMaximizedAppDelegate = item;
125+ found = true;
145126 }
146127 }
147− foregroundMaximizedAppZ = tmp;
148− foregroundMaximizedAppIndex = tmpAppId;
149− }
150−
151− function indexOf(appId) {
152− for (var i = 0; i < ApplicationManager.count; i++) {
153− if (ApplicationManager.get(i).appId == appId) {
154− return i;
155− }
128+ if (!found) {
129+ foregroundMaximizedAppDelegate = null;
156130 }
157− return -1;
158131 }
159132
160133 function minimizeAllWindows() {
@@ -164,16 +137,13 @@ AbstractStage {
164137 appDelegate.minimize();
165138 }
166139 }
167−
168− ApplicationManager.unfocusCurrentApplication(); // no app should have focus at this point
169140 }
170141
171142 function focusNext() {
172− ApplicationManager.unfocusCurrentApplication();
173143 for (var i = 0; i < appRepeater.count; i++) {
174144 var appDelegate = appRepeater.itemAt(i);
175145 if (appDelegate && !appDelegate.minimized) {
176− ApplicationManager.focusApplication(appDelegate.appId);
146+ appDelegate.focus = true;
177147 return;
178148 }
179149 }
@@ -182,15 +152,14 @@ AbstractStage {
182152
183153 Connections {
184154 target: PanelState
185− onClose: {
186− ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
155+ onClose: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
156+ onMinimize: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.minimize(); } }
157+ onMaximize: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.restoreFromMaximized(); } }
158+ onFocusMaximizedApp: {
159+ if (priv.foregroundMaximizedAppDelegate) {
160+ priv.foregroundMaximizedAppDelegate.focus = true;
161+ }
187162 }
188− onMinimize: priv.focusedAppDelegate && priv.focusedAppDelegate.minimize();
189− onMaximize: priv.focusedAppDelegate // don't restore minimized apps when double clicking the panel
190− && priv.focusedAppDelegate.restoreFromMaximized();
191− onFocusMaximizedApp: if (priv.foregroundMaximizedAppIndex != -1) {
192− ApplicationManager.focusApplication(appRepeater.itemAt(priv.foregroundMaximizedAppIndex).appId);
193− }
194163 }
195164
196165 Binding {
@@ -218,7 +187,7 @@ AbstractStage {
218187 Binding {
219188 target: PanelState
220189 property: "dropShadow"
221− value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppIndex !== -1
190+ value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppDelegate !== null
222191 }
223192
224193 Component.onDestruction: {
@@ -227,6 +196,27 @@ AbstractStage {
227196 PanelState.dropShadow = false;
228197 }
229198
199+ Instantiator {
200+ model: root.applicationManager
201+ delegate: Binding {
202+ target: model.application
203+ property: "requestedState"
204+
205+ // TODO: figure out some lifecycle policy, like suspending minimized apps
206+ // if running on a tablet or something.
207+ // TODO: If the device has a dozen suspended apps because it was running
208+ // in staged mode, when it switches to Windowed mode it will suddenly
209+ // resume all those apps at once. We might want to avoid that.
210+ value: ApplicationInfoInterface.RequestedRunning // Always running for now
211+ }
212+ }
213+
214+ Binding {
215+ target: MirFocusController
216+ property: "focusedSurface"
217+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
218+ when: !appRepeater.startingUp && root.parent
219+ }
230220
231221 FocusScope {
232222 id: appContainer
@@ -242,20 +232,25 @@ AbstractStage {
242232 fillMode: Image.PreserveAspectCrop
243233 }
244234
245− Repeater {
235+ TopLevelSurfaceRepeater {
246236 id: appRepeater
247− model: ApplicationManager
237+ model: topLevelSurfaceList
248238 objectName: "appRepeater"
249239
250240 delegate: FocusScope {
251241 id: appDelegate
252− objectName: "appDelegate_" + appId
242+ objectName: "appDelegate_" + model.id
253243 // z might be overriden in some cases by effects, but we need z ordering
254244 // to calculate occlusion detection
255− property int normalZ: ApplicationManager.count - index
245+ property int normalZ: topLevelSurfaceList.count - index
246+ onNormalZChanged: {
247+ if (visuallyMaximized) {
248+ priv.updateForegroundMaximizedApp();
249+ }
250+ }
256251 z: normalZ
257252 y: PanelState.panelHeight
258− focus: appId === priv.focusedAppId
253+
259254 width: decoratedWindow.width
260255 height: decoratedWindow.height
261256 property int requestedWidth: -1
@@ -280,36 +275,99 @@ AbstractStage {
280275 readonly property alias minimized: appDelegatePrivate.minimized
281276 readonly property alias fullscreen: decoratedWindow.fullscreen
282277
283− readonly property string appId: model.appId
278+ readonly property var application: model.application
284279 property bool animationsEnabled: true
285280 property alias title: decoratedWindow.title
286− readonly property string appName: model.name
281+ readonly property string appName: model.application ? model.application.name : ""
287282 property bool visuallyMaximized: false
288283 property bool visuallyMinimized: false
289284
285+ readonly property var surface: model.surface
286+
287+ function claimFocus() {
288+ if (spread.state == "altTab") {
289+ spread.cancel();
290+ }
291+ appDelegate.restore();
292+ }
293+ Connections {
294+ target: model.surface
295+ onFocusRequested: claimFocus();
296+ }
297+ Connections {
298+ target: model.application
299+ onFocusRequested: {
300+ if (!model.surface) {
301+ // when an app has no surfaces, we assume there's only one entry representing it:
302+ // this delegate.
303+ claimFocus();
304+ } else {
305+ // if the application has surfaces, focus request should be at surface-level.
306+ }
307+ }
308+ }
309+
290310 onFocusChanged: {
291− if (focus && ApplicationManager.focusedApplicationId !== appId) {
292− ApplicationManager.focusApplication(appId);
311+ if (appRepeater.startingUp)
312+ return;
313+
314+ if (focus) {
315+ priv.focusedAppDelegate = appDelegate;
316+
317+ // If we're orphan (!parent) it means this stage is no longer the current one
318+ // and will be deleted shortly. So we should no longer have a say over the model
319+ if (root.parent) {
320+ topLevelSurfaceList.raiseId(model.id);
321+ }
322+ } else if (!focus && priv.focusedAppDelegate === appDelegate) {
323+ priv.focusedAppDelegate = null;
324+ // FIXME: No idea why the Binding{} doens't update when focusedAppDelegate turns null
325+ MirFocusController.focusedSurface = null;
326+ }
327+ }
328+ Component.onCompleted: {
329+ // NB: We're differentiating if this delegate was created in response to a new entry in the model
330+ // or if the Repeater is just populating itself with delegates to match the model it received.
331+ if (!appRepeater.startingUp) {
332+ // a top level window is always the focused one when it first appears, unfocusing
333+ // any preexisting one
334+ focus = true;
335+ }
336+ }
337+ Component.onDestruction: {
338+ if (!root.parent) {
339+ // This stage is about to be destroyed. Don't mess up with the model at this point
340+ return;
341+ }
342+
343+ if (visuallyMaximized) {
344+ priv.updateForegroundMaximizedApp();
345+ }
346+
347+ if (focus) {
348+ // focus some other window
349+ for (var i = 0; i < appRepeater.count; i++) {
350+ var appDelegate = appRepeater.itemAt(i);
351+ if (appDelegate && !appDelegate.minimized && i != index) {
352+ appDelegate.focus = true;
353+ return;
354+ }
355+ }
293356 }
294357 }
295358
296359 onVisuallyMaximizedChanged: priv.updateForegroundMaximizedApp()
297360
298− visible: !visuallyMinimized &&
299− !greeter.fullyShown &&
300− (priv.foregroundMaximizedAppZ === -1 || priv.foregroundMaximizedAppZ <= z) ||
301− decoratedWindow.fullscreen ||
302− (spread.state == "altTab" && index === spread.highlightedIndex)
361+ visible: (
362+ !visuallyMinimized
363+ && !greeter.fullyShown
364+ && (priv.foregroundMaximizedAppDelegate === null || priv.foregroundMaximizedAppDelegate.normalZ <= z)
365+ )
366+ || decoratedWindow.fullscreen
367+ || (spread.state == "altTab" && index === spread.highlightedIndex)
303368
304− Binding {
305− target: ApplicationManager.get(index)
306− property: "requestedState"
307− // TODO: figure out some lifecycle policy, like suspending minimized apps
308− // if running on a tablet or something.
309− // TODO: If the device has a dozen suspended apps because it was running
310− // in staged mode, when it switches to Windowed mode it will suddenly
311− // resume all those apps at once. We might want to avoid that.
312− value: ApplicationInfoInterface.RequestedRunning // Always running for now
369+ function close() {
370+ model.surface.close();
313371 }
314372
315373 function maximize(animated) {
@@ -351,7 +409,8 @@ AbstractStage {
351409 maximizeLeft();
352410 else if (maximizedRight)
353411 maximizeRight();
354− ApplicationManager.focusApplication(appId);
412+
413+ focus = true;
355414 }
356415
357416 function playFocusAnimation() {
@@ -369,7 +428,7 @@ AbstractStage {
369428
370429 states: [
371430 State {
372− name: "fullscreen"; when: decoratedWindow.fullscreen
431+ name: "fullscreen"; when: decoratedWindow.fullscreen && !appDelegate.minimized
373432 PropertyChanges {
374433 target: appDelegate;
375434 x: 0;
@@ -462,6 +521,7 @@ AbstractStage {
462521 ScriptAction {
463522 script: {
464523 if (appDelegate.minimized) {
524+ appDelegate.focus = false;
465525 priv.focusNext();
466526 }
467527 }
@@ -486,7 +546,7 @@ AbstractStage {
486546 id: previewBinding
487547 target: appDelegate
488548 property: "z"
489− value: ApplicationManager.count + 1
549+ value: topLevelSurfaceList.count + 1
490550 when: index == spread.highlightedIndex && spread.ready
491551 }
492552
@@ -497,12 +557,12 @@ AbstractStage {
497557 minWidth: units.gu(10)
498558 minHeight: units.gu(10)
499559 borderThickness: units.gu(2)
500− windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
560+ windowId: model.application.appId // FIXME: Change this to point to windowId once we have such a thing
501561 screenWidth: appContainer.width
502562 screenHeight: appContainer.height
503563 leftMargin: root.leftMargin
504564
505− onPressed: { ApplicationManager.focusApplication(model.appId) }
565+ onPressed: { appDelegate.focus = true; }
506566
507567 Component.onCompleted: {
508568 loadWindowState();
@@ -529,24 +589,25 @@ AbstractStage {
529589 objectName: "decoratedWindow"
530590 anchors.left: appDelegate.left
531591 anchors.top: appDelegate.top
532− application: ApplicationManager.get(index)
533− active: ApplicationManager.focusedApplicationId === model.appId
592+ application: model.application
593+ surface: model.surface
594+ active: appDelegate.focus
534595 focus: true
535596
536597 requestedWidth: appDelegate.requestedWidth
537598 requestedHeight: appDelegate.requestedHeight
538599
539− onClose: ApplicationManager.stopApplication(model.appId)
600+ onClose: { appDelegate.close(); }
540601 onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
541602 ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
542603 onMinimize: appDelegate.minimize()
543− onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
604+ onDecorationPressed: { appDelegate.focus = true; }
544605 }
545606
546607 WindowedFullscreenPolicy {
547608 id: fullscreenPolicy
548609 active: true
549− application: decoratedWindow.application
610+ surface: model.surface
550611 }
551612 }
552613 }
qml/Stages/PhoneStage.qml+152−89
@@ -26,7 +26,6 @@ import "../Components"
2626AbstractStage {
2727 id: root
2828
29− property QtObject applicationManager: ApplicationManager
3029 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
3130 property bool altTabEnabled: true
3231 property real startScale: 1.1
@@ -107,9 +106,7 @@ AbstractStage {
107106 }
108107 }
109108
110− mainApp: applicationManager.focusedApplicationId
111− ? applicationManager.findApplication(applicationManager.focusedApplicationId)
112− : null
109+ mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
113110
114111 orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
115112 && !priv.focusedAppDelegateIsDislocated
@@ -158,72 +155,27 @@ AbstractStage {
158155 onTriggered: { root.beingResized = false; }
159156 }
160157
161− Connections {
162− target: applicationManager
163−
164− onFocusRequested: {
165− if (spreadView.phase > 0) {
166− spreadView.snapTo(priv.indexOf(appId));
167− } else {
168− applicationManager.focusApplication(appId);
169− }
170− }
171−
172− onApplicationAdded: {
173− if (spreadView.phase == 2) {
174− spreadView.snapTo(applicationManager.count - 1);
175− } else {
176− spreadView.phase = 0;
177− spreadView.contentX = -spreadView.shift;
178− applicationManager.focusApplication(appId);
179− }
180− }
181−
182− onApplicationRemoved: {
183− // Unless we're closing the app ourselves in the spread,
184− // lets make sure the spread doesn't mess up by the changing app list.
185− if (spreadView.closingIndex == -1) {
186− spreadView.phase = 0;
187− spreadView.contentX = -spreadView.shift;
188− focusTopMostApp();
189− }
190− }
191−
192− function focusTopMostApp() {
193− if (applicationManager.count > 0) {
194− var topmostApp = applicationManager.get(0);
195− applicationManager.focusApplication(topmostApp.appId);
196− }
197− }
198− }
199−
200158 QtObject {
201159 id: priv
202160
203− property string focusedAppId: root.applicationManager.focusedApplicationId
204161 property bool focusedAppOrientationChangesEnabled: false
205162 readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
206− readonly property var focusedAppDelegate: {
207− var index = indexOf(focusedAppId);
208− return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
209− }
163+ property var focusedAppDelegate
164+ // NB! This may differ from applicationManager.focusedApplicationId if focusedAppDelegate
165+ // contains a screenshot instead of a surface.
166+ property string focusedAppId: focusedAppDelegate ? focusedAppDelegate.application.appId : ""
210167
211168 property real oldInverseProgress: 0
212169 property bool animateX: false
213170 property int highlightIndex: 0
214171
215− onFocusedAppDelegateChanged: {
216− if (focusedAppDelegate) {
217− focusedAppDelegate.focus = true;
218− }
219− }
220−
221− property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
172+ property bool focusedAppDelegateIsDislocated: focusedAppDelegate ?
222173 (focusedAppDelegate.x !== 0 || focusedAppDelegate.xBehavior.running)
174+ : false
223175
224176 function indexOf(appId) {
225− for (var i = 0; i < root.applicationManager.count; i++) {
226− if (root.applicationManager.get(i).appId == appId) {
177+ for (var i = 0; i < spreadRepeater.count; i++) {
178+ if (spreadRepeater.itemAt(i).application.appId == appId) {
227179 return i;
228180 }
229181 }
@@ -237,8 +189,8 @@ AbstractStage {
237189 function reset() {
238190 // The app that's about to go to foreground has to be focused, otherwise
239191 // it would leave us in an inconsistent state.
240− if (!root.applicationManager.focusedApplicationId && root.applicationManager.count > 0) {
241− root.applicationManager.focusApplication(root.applicationManager.get(0).appId);
192+ if (!MirFocusController.focusedSurface && spreadRepeater.count > 0) {
193+ spreadRepeater.itemAt(0).focus = true;
242194 }
243195
244196 spreadView.selectedIndex = -1;
@@ -258,6 +210,36 @@ AbstractStage {
258210 }
259211 }
260212
213+ Instantiator {
214+ model: root.applicationManager
215+ delegate: QtObject {
216+ property var stateBinding: Binding {
217+ readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
218+ target: model.application
219+ property: "requestedState"
220+ value: (isDash && root.keepDashRunning)
221+ || (!root.suspended && model.application && priv.focusedAppId === model.application.appId)
222+ ? ApplicationInfoInterface.RequestedRunning
223+ : ApplicationInfoInterface.RequestedSuspended
224+ }
225+
226+ property var lifecycleBinding: Binding {
227+ target: model.application
228+ property: "exemptFromLifecycle"
229+ value: model.application
230+ ? (!model.application.isTouchApp || isExemptFromLifecycle(model.application.appId))
231+ : false
232+ }
233+ }
234+ }
235+
236+ Binding {
237+ target: MirFocusController
238+ property: "focusedSurface"
239+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
240+ when: root.parent && !spreadRepeater.startingUp
241+ }
242+
261243 Flickable {
262244 id: spreadView
263245 objectName: "spreadView"
@@ -308,13 +290,38 @@ AbstractStage {
308290 // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
309291 // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
310292 // the Flickable.boundsBehavior upon release).
311− onContentXChanged: { forceItToRemainStillIfBeingResized(); }
293+ onContentXChanged: {
294+ if (!undoContentXReset()) {
295+ forceItToRemainStillIfBeingResized();
296+ }
297+ }
312298 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
313299 function forceItToRemainStillIfBeingResized() {
314300 if (root.beingResized && contentX != -spreadView.shift) {
315301 contentX = -spreadView.shift;
316302 }
317303 }
304+ function undoContentXReset() {
305+ if (contentWidth <= 0) {
306+ contentWidthOnLastContentXChange = contentWidth;
307+ lastContentX = contentX;
308+ return false;
309+ }
310+
311+ if (contentWidth !== contentWidthOnLastContentXChange
312+ && lastContentX === -shift && contentX === 0) {
313+ // Flickable is resetting contentX because contentWidth has changed. Undo it.
314+ contentX = -shift;
315+ return true;
316+ }
317+
318+ contentWidthOnLastContentXChange = contentWidth;
319+ lastContentX = contentX;
320+ return false;
321+ }
322+ property real contentWidthOnLastContentXChange: -1
323+ property real lastContentX: 0
324+ // </FIXME-contentX>
318325
319326 Behavior on contentX {
320327 enabled: root.altTabPressed
@@ -377,7 +384,7 @@ AbstractStage {
377384 snapAnimation.start();
378385 return;
379386 }
380− if (root.applicationManager.count <= index) {
387+ if (topLevelSurfaceList.count <= index) {
381388 // In case we're trying to snap to some non existing app, lets snap back to the first one
382389 index = 0;
383390 }
@@ -414,7 +421,8 @@ AbstractStage {
414421 ScriptAction {
415422 script: {
416423 if (spreadView.selectedIndex >= 0) {
417− root.applicationManager.focusApplication(root.applicationManager.get(spreadView.selectedIndex).appId);
424+ var delegate = spreadRepeater.itemAt(spreadView.selectedIndex)
425+ delegate.focus = true;
418426
419427 spreadView.selectedIndex = -1;
420428 spreadView.phase = 0;
@@ -429,7 +437,7 @@ AbstractStage {
429437 // This width controls how much the spread can be flicked left/right. It's composed of:
430438 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
431439 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
432− width: Math.max(3, root.applicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
440+ width: Math.max(3, topLevelSurfaceList.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
433441 height: parent.height
434442 Behavior on width {
435443 enabled: spreadView.closingIndex >= 0
@@ -449,13 +457,30 @@ AbstractStage {
449457 }
450458 }
451459
452− Repeater {
460+ TopLevelSurfaceRepeater {
453461 id: spreadRepeater
454462 objectName: "spreadRepeater"
455− model: root.applicationManager
463+ model: topLevelSurfaceList
464+
465+ onItemRemoved: {
466+ // Unless we're closing the app ourselves in the spread,
467+ // lets make sure the spread doesn't mess up by the changing app list.
468+ if (spreadView.closingIndex == -1) {
469+ spreadView.phase = 0;
470+ spreadView.contentX = -spreadView.shift;
471+ focusTopMostApp();
472+ }
473+ }
474+ function focusTopMostApp() {
475+ if (spreadRepeater.count > 0) {
476+ var topmostDelegate = spreadRepeater.itemAt(0);
477+ topmostDelegate.focus = true;
478+ }
479+ }
480+
456481 delegate: TransformedSpreadDelegate {
457482 id: appDelegate
458− objectName: "appDelegate" + index
483+ objectName: "spreadDelegate_" + model.id
459484 startAngle: 45
460485 endAngle: 5
461486 startScale: root.startScale
@@ -467,28 +492,61 @@ AbstractStage {
467492 selected: spreadView.selectedIndex == index
468493 otherSelected: spreadView.selectedIndex >= 0 && !selected
469494 interactive: !spreadView.interactive && spreadView.phase === 0
470− && priv.fullyShowingFocusedApp && root.interactive && isFocused
495+ && priv.fullyShowingFocusedApp && root.interactive && focus
471496 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
472497 maximizedAppTopMargin: root.maximizedAppTopMargin
473498 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
474499 focusFirstApp: root.focusFirstApp
475500 highlightShown: root.altTabPressed && index === priv.highlightIndex
476501
477− readonly property bool isDash: model.appId == "unity8-dash"
478−
479− Binding {
480− target: appDelegate.application
481− property: "exemptFromLifecycle"
482− value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
502+ readonly property bool isDash: model.application.appId == "unity8-dash"
503+
504+ Component.onCompleted: {
505+ // NB: We're differentiating if this delegate was created in response to a new entry in the model
506+ // or if the Repeater is just populating itself with delegates to match the model it received.
507+ if (!spreadRepeater.startingUp) {
508+ // a top level window is always the focused one when it first appears, unfocusing
509+ // any preexisting one
510+ //
511+ // new items are appended and must be manually brought to front.
512+ // that's how it *must* be in order to get the animation for new
513+ // surfaces working
514+ claimFocus();
515+ }
483516 }
484517
485− Binding {
486− target: appDelegate.application
487− property: "requestedState"
488− value: (isDash && root.keepDashRunning)
489− || (!root.suspended && appDelegate.focus)
490− ? ApplicationInfoInterface.RequestedRunning
491− : ApplicationInfoInterface.RequestedSuspended
518+ onFocusChanged: {
519+ if (focus && !spreadRepeater.startingUp) {
520+ priv.focusedAppDelegate = appDelegate;
521+ // If we're orphan (!parent) it means this stage is no longer the current one
522+ // and will be deleted shortly. So we should no longer have a say over the model
523+ if (root.parent) {
524+ topLevelSurfaceList.raiseId(model.id);
525+ }
526+ }
527+ }
528+ function claimFocus() {
529+ if (spreadView.phase > 0) {
530+ spreadView.snapTo(model.index);
531+ } else {
532+ appDelegate.focus = true;
533+ }
534+ }
535+ Connections {
536+ target: model.surface
537+ onFocusRequested: claimFocus()
538+ }
539+ Connections {
540+ target: model.application
541+ onFocusRequested: {
542+ if (!model.surface) {
543+ // when an app has no surfaces, we assume there's only one entry representing it:
544+ // this delegate.
545+ claimFocus();
546+ } else {
547+ // if the application has surfaces, focus request should be at surface-level.
548+ }
549+ }
492550 }
493551
494552 z: isDash && !spreadView.active ? -1 : behavioredIndex
@@ -509,7 +567,8 @@ AbstractStage {
509567 return spreadView.width + spreadIndex * spreadView.tileDistance;
510568 }
511569
512− application: root.applicationManager.get(index)
570+ application: model.application
571+ surface: model.surface
513572 closeable: !isDash
514573
515574 property real behavioredIndex: index
@@ -593,11 +652,7 @@ AbstractStage {
593652
594653 onClicked: {
595654 if (root.altTabEnabled && spreadView.phase == 2) {
596− if (root.applicationManager.focusedApplicationId == root.applicationManager.get(index).appId) {
597− spreadView.snapTo(index);
598− } else {
599− root.applicationManager.requestFocusApplication(root.applicationManager.get(index).appId);
600− }
655+ spreadView.snapTo(index);
601656 }
602657 }
603658
@@ -611,7 +666,15 @@ AbstractStage {
611666
612667 onClosed: {
613668 spreadView.closingIndex = index;
614− root.applicationManager.stopApplication(root.applicationManager.get(index).appId);
669+ if (appDelegate.surface) {
670+ appDelegate.surface.close();
671+ } else if (appDelegate.application) {
672+ root.applicationManager.stopApplication(appDelegate.application.appId);
673+ } else {
674+ // should never happen
675+ console.warn("Can't close topLevelSurfaceList entry as it has neither"
676+ + " a surface nor an application");
677+ }
615678 }
616679
617680 Binding {
@@ -629,7 +692,7 @@ AbstractStage {
629692
630693 StagedFullscreenPolicy {
631694 id: fullscreenPolicy
632− application: appDelegate.application
695+ surface: model.surface
633696 }
634697
635698 Connections {
@@ -637,7 +700,7 @@ AbstractStage {
637700 onStageAboutToBeUnloaded: fullscreenPolicy.active = false
638701 }
639702 }
640− }
703+ } // Repeater {
641704 }
642705 }
643706
qml/Stages/PromptSurfaceAnimations.qmlnew file+81
@@ -0,0 +1,81 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+*/
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+
20+StateGroup {
21+ id: root
22+ property var container
23+ property var surfaceItem
24+
25+ states: [
26+ State {
27+ name: "blank"
28+ when: !root.surfaceItem.surface
29+ },
30+ State {
31+ name: "ready"
32+ when: root.surfaceItem.surface && root.surfaceItem.live
33+ },
34+ State {
35+ name: "zombie"
36+ when: root.surfaceItem.surface && !root.surfaceItem.live
37+ }
38+ ]
39+ transitions: [
40+ Transition {
41+ from: "*"; to: "zombie"
42+ // Slide downwards until it's out of view, through the bottom of the window
43+ SequentialAnimation {
44+ // clip so we don't go out of parent's bounds during spread
45+ PropertyAction { target: root.container.parent; property: "clip"; value: true }
46+ UbuntuNumberAnimation { target: root.surfaceItem; property: "y"; to: root.container.height
47+ duration: UbuntuAnimation.BriskDuration }
48+ PropertyAction { target: root.surfaceItem; property: "visible"; value: false }
49+ PropertyAction { target: container.parent; property: "clip"; value: false }
50+ ScriptAction { script: {
51+ // Unity.Application can't destroy a zombie MirSurface if it's still being
52+ // referenced by a MirSurfaceItem.
53+ root.surfaceItem.surface = null;
54+ } }
55+ }
56+ },
57+ Transition {
58+ from: "*"; to: "ready"
59+ // Slide upwards into view, from the bottom of the window
60+ SequentialAnimation {
61+ // clip so we don't go out of parent's bounds during spread
62+ PropertyAction { target: root.container.parent; property: "clip"; value: true }
63+ ScriptAction { script: {
64+ root.surfaceItem.y = root.container.height;
65+ root.surfaceItem.visible = true;
66+ } }
67+ UbuntuNumberAnimation {
68+ target: root.surfaceItem; property: "y"; to: 0
69+ duration: UbuntuAnimation.BriskDuration
70+ }
71+ PropertyAction { target: container.parent; property: "clip"; value: false }
72+ }
73+ },
74+ Transition {
75+ from: "*"; to: "blank"
76+ ScriptAction { script: {
77+ root.surfaceItem.visible = false;
78+ } }
79+ }
80+ ]
81+}
qml/Stages/SessionContainer.qmldeleted−180
@@ -1,180 +0,0 @@
1−/*
2− * Copyright 2014-2015 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU Lesser General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15−*/
16−
17−import QtQuick 2.4
18−import "Animations"
19−
20−FocusScope {
21− id: root
22− objectName: "sessionContainer"
23− implicitWidth: _surfaceContainer.implicitWidth
24− implicitHeight: _surfaceContainer.implicitHeight
25− property QtObject session
26− readonly property var childSessions: session ? session.childSessions : null
27− readonly property alias surface: _surfaceContainer.surface
28− property alias interactive: _surfaceContainer.interactive
29− property alias surfaceOrientationAngle: _surfaceContainer.surfaceOrientationAngle
30− property alias resizeSurface: _surfaceContainer.resizeSurface
31−
32− property int requestedWidth: -1
33− property int requestedHeight: -1
34−
35− readonly property alias surfaceContainer: _surfaceContainer
36− SurfaceContainer {
37− id: _surfaceContainer
38− requestedWidth: root.requestedWidth
39− requestedHeight: root.requestedHeight
40− surface: session ? session.lastSurface : null
41− }
42−
43− // SurfaceContainer size drives SessionContainer size
44− Binding {
45− target: root; property: "width"; value: _surfaceContainer.width
46− when: root.requestedWidth >= 0
47− }
48− Binding {
49− target: root; property: "height"; value: _surfaceContainer.height
50− when: root.requestedHeight >= 0
51− }
52−
53− // SessionContainer size drives SurfaceContainer size
54− Binding {
55− target: _surfaceContainer; property: "width"; value: root.width
56− when: root.requestedWidth < 0
57− }
58− Binding {
59− target: _surfaceContainer; property: "height"; value: root.height
60− when: root.requestedHeight < 0
61− }
62−
63− Repeater {
64− id: childSessionsRepeater
65− model: root.childSessions
66−
67− delegate: Loader {
68− objectName: "childDelegate" + index
69− anchors.fill: surfaceContainer
70−
71− // Only way to do recursive qml items.
72− source: Qt.resolvedUrl("SessionContainer.qml")
73−
74− z: index
75−
76− // Since a Loader is a FocusScope, propagate its focus to the loaded Item
77− Binding {
78− target: item; when: item
79− property: "focus"; value: focus
80− }
81−
82− Binding {
83− target: item; when: item
84− property: "interactive"; value: index == (childSessionsRepeater.count - 1) && root.interactive
85− }
86−
87− Binding {
88− target: item; when: item
89− property: "session"; value: modelData
90− }
91−
92− Binding {
93− target: item; when: item
94− property: "width"; value: root.width
95− }
96−
97− Binding {
98− target: item; when: item
99− property: "height"; value: root.height
100− }
101− }
102− }
103−
104− states: [
105− State {
106− name: "rootSession"
107− when: root.session && !root.session.parentSession
108− },
109−
110− State {
111− name: "childSession"
112− when: root.session && root.session.parentSession !== null && root.session.live
113− && !root.session.lastSurface
114− },
115−
116− State {
117− name: "childSessionReady"
118− when: root.session && root.session.parentSession !== null && root.session.live
119− && root.session.lastSurface !== null
120− },
121−
122− State {
123− name: "childSessionZombie"
124− when: root.session && root.session.parentSession !== null && !root.session.live
125− }
126− ]
127−
128− transitions: [
129− Transition {
130− to: "childSessionReady"
131− ScriptAction { script: { if (!surfaceContainer.hadSurface) { animateIn(swipeFromBottom); } } }
132− },
133− Transition {
134− to: "childSessionZombie"
135− ScriptAction { script: { animateOut(); } }
136− }
137− ]
138−
139− function animateIn(component) {
140− var animation = component.createObject(root, { "container": root, });
141− animation.start();
142−
143− var tmp = d.animations;
144− tmp.push(animation);
145− d.animations = tmp;
146− }
147−
148− function animateOut() {
149− if (d.animations.length > 0) {
150− var tmp = d.animations;
151− var popped = tmp.pop();
152− popped.completed.connect(function() { root.session.release(); } );
153− popped.end();
154− d.animations = tmp;
155− } else {
156− root.session.release();
157− }
158− }
159−
160− Component {
161− id: swipeFromBottom
162− SwipeFromBottomAnimation {}
163− }
164−
165− QtObject {
166− id: d
167− property var animations: []
168−
169− property var focusedChild: {
170− if (childSessionsRepeater.count == 0) {
171− return _surfaceContainer;
172− } else {
173− return childSessionsRepeater.itemAt(childSessionsRepeater.count - 1);
174− }
175− }
176− onFocusedChildChanged: {
177− focusedChild.focus = true;
178− }
179− }
180−}
qml/Stages/SpreadDelegate.qml+4−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -45,6 +45,7 @@ FocusScope {
4545 property alias swipeToCloseEnabled: dragArea.enabled
4646 property bool closeable
4747 property alias application: appWindow.application
48+ property alias surface: appWindow.surface
4849 property int shellOrientationAngle
4950 property int shellOrientation
5051 property QtObject orientations
@@ -250,7 +251,7 @@ FocusScope {
250251 }
251252 PropertyChanges {
252253 target: appWindow
253− surfaceOrientationAngle: orientationAngle
254+ surfaceOrientationAngle: appWindowWithShadow.orientationAngle
254255 }
255256 },
256257 State {
@@ -292,7 +293,7 @@ FocusScope {
292293
293294 ApplicationWindow {
294295 id: appWindow
295− objectName: application ? "appWindow_" + application.appId : "appWindow_null"
296+ objectName: "appWindow"
296297 focus: true
297298 anchors {
298299 fill: parent
qml/Stages/StagedFullscreenPolicy.qml+12−14
@@ -27,31 +27,29 @@ import Unity.Application 0.1
2727// Chrome not set and state change to fulscreen -> client window stays "fullscreen"
2828QtObject {
2929 property bool active: true
30− property QtObject application: null
3130
32− readonly property var lastSurface: application && application.session ?
33− application.session.lastSurface : null
34− onLastSurfaceChanged: {
35− if (!active || !lastSurface) return;
36− if (lastSurface.shellChrome === Mir.LowChrome) {
37− lastSurface.state = Mir.FullscreenState;
31+ property var surface: null
32+ onSurfaceChanged: {
33+ if (!active || !surface) return;
34+ if (surface.shellChrome === Mir.LowChrome) {
35+ surface.state = Mir.FullscreenState;
3836 }
3937 }
4038
4139 property var _connections: Connections {
42− target: lastSurface
40+ target: surface
4341 onShellChromeChanged: {
44− if (!active || !lastSurface) return;
45− if (lastSurface.shellChrome === Mir.LowChrome) {
46− lastSurface.state = Mir.FullscreenState;
42+ if (!active || !surface) return;
43+ if (surface.shellChrome === Mir.LowChrome) {
44+ surface.state = Mir.FullscreenState;
4745 } else {
48− lastSurface.state = Mir.RestoredState;
46+ surface.state = Mir.RestoredState;
4947 }
5048 }
5149 onStateChanged: {
5250 if (!active) return;
53− if (lastSurface.state === Mir.RestoredState && lastSurface.shellChrome === Mir.LowChrome) {
54− lastSurface.state = Mir.FullscreenState;
51+ if (surface.state === Mir.RestoredState && surface.shellChrome === Mir.LowChrome) {
52+ surface.state = Mir.FullscreenState;
5553 }
5654 }
5755 }
qml/Stages/SurfaceContainer.qml+97−40
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -24,21 +24,22 @@ FocusScope {
2424 id: root
2525 objectName: "surfaceContainer"
2626
27+ // Must be set from outside
2728 property var surface: null
28− property bool hadSurface: false
29− property bool interactive
30− property int surfaceOrientationAngle: 0
31− property string name: surface ? surface.name : ""
32− property bool resizeSurface: true
3329
30+ // Might be changed from outside
3431 property int requestedWidth: -1
3532 property int requestedHeight: -1
33+ property bool interactive
34+ property int surfaceOrientationAngle: 0
35+ property bool resizeSurface: true
36+ property bool inPromptSession: false
3637
3738 onSurfaceChanged: {
38− if (surface) {
39− surfaceItem.surface = surface;
40− root.hadSurface = false;
41− }
39+ // Not a binding because animations might remove the surface from the surfaceItem
40+ // programatically (in order to signal that a zombie surface is free for deletion),
41+ // even though root.surface is still !null.
42+ surfaceItem.surface = surface;
4243 }
4344
4445 InputWatcher {
@@ -83,11 +84,16 @@ FocusScope {
8384 }
8485
8586 enabled: root.interactive
86− focus: true
8787 antialiasing: !root.interactive
8888 orientationAngle: root.surfaceOrientationAngle
8989 }
9090
91+ TouchGate {
92+ targetItem: surfaceItem
93+ anchors.fill: root
94+ enabled: surfaceItem.enabled
95+ }
96+
9197 // MirSurface size drives SurfaceContainer size
9298 Binding {
9399 target: surfaceItem; property: "width"; value: root.surface ? root.surface.size.width : 0
@@ -116,39 +122,90 @@ FocusScope {
116122 when: root.requestedHeight < 0
117123 }
118124
125+ Repeater {
126+ id: childSurfacesRepeater
127+ objectName: "childSurfacesRepeater"
128+ model: root.surface ? root.surface.promptSurfaceList : null
119129
120− TouchGate {
121− objectName: "touchGate-"+name
122− targetItem: surfaceItem
123− anchors.fill: root
124− enabled: surfaceItem.enabled
130+ delegate: Loader {
131+ objectName: "childDelegate" + index
132+ anchors.fill: root
133+
134+ // Only way to do recursive qml items.
135+ source: Qt.resolvedUrl("SurfaceContainer.qml")
136+
137+ z: index
138+
139+ // Since a Loader is a FocusScope, propagate its focus to the loaded Item
140+ Binding {
141+ target: item; when: item
142+ property: "focus"; value: focus
143+ }
144+
145+ Binding {
146+ target: item; when: item
147+ property: "interactive"; value: index == (childSurfacesRepeater.count - 1) && root.interactive
148+ }
149+
150+ Binding {
151+ target: item; when: item
152+ property: "surface"; value: model.surface
153+ }
154+
155+ Binding {
156+ target: item; when: item
157+ property: "width"; value: root.width
158+ }
159+
160+ Binding {
161+ target: item; when: item
162+ property: "height"; value: root.height
163+ }
164+
165+ Binding {
166+ target: item; when: item
167+ property: "inPromptSession"; value: true
168+ }
169+ }
125170 }
126171
127− states: [
128− State {
129− name: "zombie"
130− when: surfaceItem.surface && !surfaceItem.live
172+ Loader {
173+ id: animationsLoader
174+ objectName: "animationsLoader"
175+ active: root.surface
176+ source: {
177+ if (root.inPromptSession) {
178+ return "PromptSurfaceAnimations.qml";
179+ } else {
180+ // Let ApplicationWindow do the animations
181+ return "";
182+ }
183+ }
184+ Binding {
185+ target: animationsLoader.item
186+ when: animationsLoader.item
187+ property: "surfaceItem"
188+ value: surfaceItem
189+ }
190+ Binding {
191+ target: animationsLoader.item
192+ when: animationsLoader.item
193+ property: "container"
194+ value: root
131195 }
132− ]
133− transitions: [
134− Transition {
135− from: ""; to: "zombie"
136− SequentialAnimation {
137− UbuntuNumberAnimation { target: surfaceItem; property: "opacity"; to: 0.0
138− duration: UbuntuAnimation.BriskDuration }
139− PropertyAction { target: surfaceItem; property: "visible"; value: false }
140− ScriptAction { script: {
141− surfaceItem.surface = null;
142− root.hadSurface = true;
143− } }
196+ }
197+
198+ QtObject {
199+ id: d
200+ property var focusedChild: {
201+ if (childSurfacesRepeater.count == 0) {
202+ return surfaceItem;
203+ } else {
204+ return childSurfacesRepeater.itemAt(childSurfacesRepeater.count - 1);
144205 }
145− },
146− Transition {
147− from: "zombie"; to: ""
148− ScriptAction { script: {
149− surfaceItem.opacity = 1.0;
150− surfaceItem.visible = true;
151− } }
152206 }
153− ]
207+ onFocusedChildChanged: {
208+ focusedChild.focus = true;
209+ }
210+ }
154211}
qml/Stages/TabletStage.qml+300−237
@@ -27,19 +27,24 @@ AbstractStage {
2727 objectName: "stages"
2828 anchors.fill: parent
2929
30+ // <tutorial-hacks> The Tutorial looks into our implementation details
3031 property alias sideStageVisible: spreadView.sideStageVisible
3132 property alias sideStageWidth: spreadView.sideStageWidth
33+ // The stage the currently focused surface is in
34+ property int stageFocusedSurface: priv.focusedAppDelegate ? priv.focusedAppDelegate.stage : ApplicationInfoInterface.MainStage
35+ // </tutorial-hacks>
3236
3337 // Functions to be called from outside
3438 function updateFocusedAppOrientation() {
35− var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
36− if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
37− spreadRepeater.itemAt(mainStageAppIndex).matchShellOrientation();
39+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
40+
41+ if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
42+ spreadRepeater.itemAt(mainStageIndex).matchShellOrientation();
3843 }
3944
4045 for (var i = 0; i < spreadRepeater.count; ++i) {
4146
42− if (i === mainStageAppIndex) {
47+ if (i === mainStageIndex) {
4348 continue;
4449 }
4550
@@ -60,16 +65,14 @@ AbstractStage {
6065 }
6166 }
6267 function updateFocusedAppOrientationAnimated() {
63− var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
64− if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
65− spreadRepeater.itemAt(mainStageAppIndex).animateToShellOrientation();
68+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
69+ if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
70+ spreadRepeater.itemAt(mainStageIndex).animateToShellOrientation();
6671 }
6772
68− if (priv.sideStageAppId) {
69− var sideStageAppIndex = priv.indexOf(priv.sideStageAppId);
70− if (sideStageAppIndex >= 0 && sideStageAppIndex < spreadRepeater.count) {
71− spreadRepeater.itemAt(sideStageAppIndex).matchShellOrientation();
72− }
73+ var sideStageIndex = root.topLevelSurfaceList.indexForId(priv.sideStageItemId);
74+ if (sideStageIndex >= 0 && sideStageIndex < spreadRepeater.count) {
75+ spreadRepeater.itemAt(sideStageIndex).matchShellOrientation();
7376 }
7477 }
7578
@@ -81,11 +84,20 @@ AbstractStage {
8184
8285 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
8386
87+ mainApp: {
88+ if (priv.mainStageItemId > 0) {
89+ var index = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
90+ return root.topLevelSurfaceList.applicationAt(index);
91+ } else {
92+ return null;
93+ }
94+ }
95+
8496 supportedOrientations: {
8597 if (mainApp) {
8698 var orientations = mainApp.supportedOrientations;
8799 orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
88− if (priv.sideStageAppId && !spreadView.surfaceDragging) {
100+ if (priv.sideStageItemId && !spreadView.surfaceDragging) {
89101 // If we have a sidestage app, support Portrait orientation
90102 // so that it will switch the sidestage app to mainstage on rotate
91103 orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
@@ -116,7 +128,7 @@ AbstractStage {
116128 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
117129 // left edge drag released. Minimum distance is given by design.
118130 if (priv.oldInverseProgress > units.gu(22)) {
119− ApplicationManager.requestFocusApplication("unity8-dash");
131+ root.applicationManager.requestFocusApplication("unity8-dash");
120132 }
121133 }
122134 priv.oldInverseProgress = inverseProgress;
@@ -154,17 +166,51 @@ AbstractStage {
154166 }
155167 }
156168
169+ Connections {
170+ target: root.topLevelSurfaceList
171+ onListChanged: priv.updateMainAndSideStageIndexes()
172+ }
173+
157174 QtObject {
158175 id: priv
159176 objectName: "stagesPriv"
160177
161− property string focusedAppId: ApplicationManager.focusedApplicationId
162− readonly property var focusedAppDelegate: {
163− var index = indexOf(focusedAppId);
164− return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
178+ function updateMainAndSideStageIndexes() {
179+ var choseMainStage = false;
180+ var choseSideStage = false;
181+
182+ if (!root.topLevelSurfaceList)
183+ return;
184+
185+ for (var i = 0; i < spreadRepeater.count && (!choseMainStage || !choseSideStage); ++i) {
186+ var spreadDelegate = spreadRepeater.itemAt(i);
187+ if (sideStage.shown && spreadDelegate.stage == ApplicationInfoInterface.SideStage
188+ && !choseSideStage) {
189+ priv.sideStageDelegate = spreadDelegate
190+ priv.sideStageItemId = root.topLevelSurfaceList.idAt(i);
191+ priv.sideStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
192+ choseSideStage = true;
193+ } else if (!choseMainStage && spreadDelegate.stage == ApplicationInfoInterface.MainStage) {
194+ priv.mainStageDelegate = spreadDelegate;
195+ priv.mainStageItemId = root.topLevelSurfaceList.idAt(i);
196+ priv.mainStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
197+ choseMainStage = true;
198+ }
199+ }
200+ if (!choseMainStage) {
201+ priv.mainStageDelegate = null;
202+ priv.mainStageItemId = 0;
203+ priv.mainStageAppId = "";
204+ }
205+ if (!choseSideStage) {
206+ priv.sideStageDelegate = null;
207+ priv.sideStageItemId = 0;
208+ priv.sideStageAppId = "";
209+ }
165210 }
166211
167− property string oldFocusedAppId: ""
212+ property var focusedAppDelegate: null
213+
168214 property bool mainAppOrientationChangesEnabled: false
169215
170216 property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
@@ -173,36 +219,21 @@ AbstractStage {
173219 property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
174220 || root.shellOrientation === Qt.InvertedLandscapeOrientation
175221
176− property string mainStageAppId
177− property string sideStageAppId
222+ property var mainStageDelegate: null
223+ property var sideStageDelegate: null
178224
179− // For convenience, keep properties of the first two apps in the model
180− property string appId0
181− property string appId1
225+ property int mainStageItemId: 0
226+ property int sideStageItemId: 0
227+
228+ property string mainStageAppId: ""
229+ property string sideStageAppId: ""
182230
183231 property int oldInverseProgress: 0
184232
185233 property int highlightIndex: 0
186234
187− onFocusedAppIdChanged: updateStageApps()
188−
189− onFocusedAppDelegateChanged: {
190− if (focusedAppDelegate) {
191− focusedAppDelegate.focus = true;
192− }
193− }
194−
195235 property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
196236 (focusedAppDelegate.dragOffset !== 0 || focusedAppDelegate.xTranslateAnimating)
197− function indexOf(appId) {
198− for (var i = 0; i < ApplicationManager.count; i++) {
199− if (ApplicationManager.get(i).appId == appId) {
200− return i;
201− }
202− }
203− return -1;
204− }
205−
206237 function evaluateOneWayFlick(gesturePoints) {
207238 // Need to have at least 3 points to recognize it as a flick
208239 if (gesturePoints.length < 3) {
@@ -231,88 +262,45 @@ AbstractStage {
231262 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
232263 }
233264
234− function getTopApp(stage) {
235− for (var i = 0; i < ApplicationManager.count; i++) {
236− var app = ApplicationManager.get(i)
237− if (app.stage === stage) {
238− return app;
239− }
240− }
241− return null;
242− }
243−
244− function setAppStage(appId, stage, save) {
245− var app = ApplicationManager.findApplication(appId);
246− if (app) {
247− app.stage = stage;
248− if (save) {
249− WindowStateStorage.saveStage(appId, stage);
250− }
251− }
252− }
253−
254− function updateStageApps() {
255− var app = priv.getTopApp(ApplicationInfoInterface.MainStage);
256− priv.mainStageAppId = app ? app.appId : ""
257− root.mainApp = app;
258−
259− if (sideStage.shown) {
260− app = priv.getTopApp(ApplicationInfoInterface.SideStage);
261− priv.sideStageAppId = app ? app.appId : ""
262− } else {
263− priv.sideStageAppId = "";
264− }
265−
266− appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
267− appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
268− }
269−
270265 readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
271266 root.shellOrientation == Qt.InvertedLandscapeOrientation
272− Component.onCompleted: updateStageApps();
273267 }
274268
275− Connections {
276− target: ApplicationManager
277− onFocusRequested: {
278− if (spreadView.interactive) {
279− spreadView.snapTo(priv.indexOf(appId));
280− } else {
281− ApplicationManager.focusApplication(appId);
269+ Instantiator {
270+ model: root.applicationManager
271+ delegate: QtObject {
272+ property var stateBinding: Binding {
273+ readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
274+ target: model.application
275+ property: "requestedState"
276+
277+ // NB: the first application clause is just to ensure we never get warnings for trying to access
278+ // members of a null variable.
279+ value: model.application &&
280+ (
281+ (isDash && root.keepDashRunning)
282+ || (!root.suspended && (model.application.appId === priv.mainStageAppId
283+ || model.application.appId === priv.sideStageAppId))
284+ )
285+ ? ApplicationInfoInterface.RequestedRunning
286+ : ApplicationInfoInterface.RequestedSuspended
282287 }
283− }
284288
285− onApplicationAdded: {
286− if (spreadView.phase == 2) {
287− spreadView.snapTo(ApplicationManager.count - 1);
288− } else {
289− spreadView.phase = 0;
290− spreadView.contentX = -spreadView.shift;
291− ApplicationManager.focusApplication(appId);
289+ property var lifecycleBinding: Binding {
290+ target: model.application
291+ property: "exemptFromLifecycle"
292+ value: model.application
293+ ? (!model.application.isTouchApp || isExemptFromLifecycle(model.application.appId))
294+ : false
292295 }
293296 }
297+ }
294298
295− onApplicationRemoved: {
296− if (priv.mainStageAppId == appId) {
297− ApplicationManager.focusApplication("unity8-dash")
298− }
299− if (priv.sideStageAppId == appId) {
300− var app = priv.getTopApp(ApplicationInfoInterface.SideStage);
301− priv.sideStageAppId = app === null ? "" : app.appId;
302− }
303−
304− if (ApplicationManager.count == 0) {
305− spreadView.phase = 0;
306− spreadView.contentX = -spreadView.shift;
307− } else if (spreadView.closingIndex == -1) {
308− // Unless we're closing the app ourselves in the spread,
309− // lets make sure the spread doesn't mess up by the changing app list.
310− spreadView.phase = 0;
311− spreadView.contentX = -spreadView.shift;
312−
313− ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
314− }
315− }
299+ Binding {
300+ target: MirFocusController
301+ property: "focusedSurface"
302+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
303+ when: root.parent && !spreadRepeater.startingUp
316304 }
317305
318306 Flickable {
@@ -363,16 +351,45 @@ AbstractStage {
363351 property int selectedIndex: -1
364352 property int draggedDelegateCount: 0
365353 property int closingIndex: -1
366− property var selectedApplication: selectedIndex !== -1 ? ApplicationManager.get(selectedIndex) : null
367−
368− // FIXME: Workaround Flickable's not keepping its contentX still when resized
369− onContentXChanged: { forceItToRemainStillIfBeingResized(); }
354+ property var selectedDelegate: selectedIndex !== -1 ? spreadRepeater.itemAt(selectedIndex) : null
355+
356+ // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
357+ // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
358+ // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
359+ // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
360+ // the Flickable.boundsBehavior upon release).
361+ onContentXChanged: {
362+ if (!undoContentXReset()) {
363+ forceItToRemainStillIfBeingResized();
364+ }
365+ }
370366 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
371367 function forceItToRemainStillIfBeingResized() {
372− if (root.beingResized && contentX != -shift) {
368+ if (root.beingResized && contentX != -spreadView.shift) {
369+ contentX = -spreadView.shift;
370+ }
371+ }
372+ function undoContentXReset() {
373+ if (contentWidth <= 0) {
374+ contentWidthOnLastContentXChange = contentWidth;
375+ lastContentX = contentX;
376+ return false;
377+ }
378+
379+ if (contentWidth != contentWidthOnLastContentXChange
380+ && lastContentX == -shift && contentX == 0) {
381+ // Flickable is resetting contentX because contentWidth has changed. Undo it.
373382 contentX = -shift;
383+ return true;
374384 }
385+
386+ contentWidthOnLastContentXChange = contentWidth;
387+ lastContentX = contentX;
388+ return false;
375389 }
390+ property real contentWidthOnLastContentXChange: -1
391+ property real lastContentX: 0
392+ // </FIXME-contentX>
376393
377394 property bool animateX: true
378395 property bool beingResized: root.beingResized
@@ -385,31 +402,33 @@ AbstractStage {
385402 }
386403 }
387404
388− property real sideStageDragProgress: sideStage.progress
389− property bool sideStageVisible: priv.sideStageAppId
390405 property real sideStageWidth: units.gu(40)
391406
392407 property bool surfaceDragging: triGestureArea.recognisedDrag
393408
394− // In case the ApplicationManager already holds an app when starting up we're missing animations
409+ readonly property bool sideStageVisible: priv.sideStageItemId != 0
410+
411+ // In case applicationManager already holds an app when starting up we're missing animations
395412 // Make sure we end up in the same state
396413 Component.onCompleted: {
397414 spreadView.contentX = -spreadView.shift
398415 }
399416
400417 property int nextInStack: {
418+ var mainStageIndex = priv.mainStageDelegate ? priv.mainStageDelegate.index : -1;
419+ var sideStageIndex = priv.sideStageDelegate ? priv.sideStageDelegate.index : -1;
401420 switch (state) {
402421 case "main":
403− if (ApplicationManager.count > 1) {
422+ if (root.topLevelSurfaceList.count > 1) {
404423 return 1;
405424 }
406425 return -1;
407426 case "mainAndOverlay":
408− if (ApplicationManager.count <= 2) {
427+ if (root.topLevelSurfaceList.count <= 2) {
409428 return -1;
410429 }
411− if (priv.appId0 == priv.mainStageAppId || priv.appId0 == priv.sideStageAppId) {
412− if (priv.appId1 == priv.mainStageAppId || priv.appId1 == priv.sideStageAppId) {
430+ if (mainStageIndex == 0 || sideStageIndex == 0) {
431+ if (mainStageIndex == 1 || sideStageIndex == 1) {
413432 return 2;
414433 }
415434 return 1;
@@ -420,7 +439,7 @@ AbstractStage {
420439 }
421440 return -1;
422441 }
423− property int nextZInStack: indexToZIndex(nextInStack)
442+ property int nextZInStack
424443
425444 states: [
426445 State {
@@ -440,13 +459,13 @@ AbstractStage {
440459 }
441460 ]
442461 state: {
443− if ((priv.mainStageAppId && !priv.sideStageAppId) || !priv.sideStageEnabled) {
462+ if ((priv.mainStageItemId && !priv.sideStageItemId) || !priv.sideStageEnabled) {
444463 return "main";
445464 }
446− if (!priv.mainStageAppId && priv.sideStageAppId) {
465+ if (!priv.mainStageItemId && priv.sideStageItemId) {
447466 return "overlay";
448467 }
449− if (priv.mainStageAppId && priv.sideStageAppId) {
468+ if (priv.mainStageItemId && priv.sideStageItemId) {
450469 return "mainAndOverlay";
451470 }
452471 return "empty";
@@ -511,38 +530,41 @@ AbstractStage {
511530 // only shuffle when we've got a main and overlay
512531 if (state !== "mainAndOverlay") return index;
513532
514− var app = ApplicationManager.get(index);
533+ var app = root.topLevelSurfaceList.applicationAt(index);
515534 if (!app) {
516535 return index;
517536 }
537+ var stage = spreadRepeater.itemAt(index) ? spreadRepeater.itemAt(index).stage : app.stage;
518538
519539 // don't shuffle indexes greater than "actives or next"
520540 if (index > 2) return index;
521541
522− if (app.appId === priv.mainStageAppId) {
542+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
543+
544+ if (index == mainStageIndex) {
523545 // Active main stage always at 0
524546 return 0;
525547 }
526548
527549 if (spreadView.nextInStack > 0) {
528− var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
550+ var stageOfNextInStack = spreadRepeater.itemAt(spreadView.nextInStack).stage;
529551
530552 if (index === spreadView.nextInStack) {
531553 // this is the next app in stack.
532554
533− if (app.stage === ApplicationInfoInterface.SideStage) {
555+ if (stage === ApplicationInfoInterface.SideStage) {
534556 // if the next app in stack is a sidestage app, it must order on top of other side stage app
535− return Math.min(2, ApplicationManager.count-1);
557+ return Math.min(2, root.topLevelSurfaceList.count-1);
536558 }
537559 return 1;
538560 }
539− if (nextAppInStack.stage === ApplicationInfoInterface.SideStage) {
561+ if (stageOfNextInStack === ApplicationInfoInterface.SideStage) {
540562 // if the next app in stack is a sidestage app, it must order on top of other side stage app
541563 return 1;
542564 }
543− return Math.min(2, ApplicationManager.count-1);
565+ return Math.min(2, root.topLevelSurfaceList.count-1);
544566 }
545− return Math.min(index+1, ApplicationManager.count-1);
567+ return Math.min(index+1, root.topLevelSurfaceList.count-1);
546568 }
547569
548570 SequentialAnimation {
@@ -560,12 +582,13 @@ AbstractStage {
560582 script: {
561583 if (spreadView.selectedIndex >= 0) {
562584 var newIndex = spreadView.selectedIndex;
563− var application = ApplicationManager.get(newIndex);
564− if (application.stage === ApplicationInfoInterface.SideStage) {
585+ var application = root.topLevelSurfaceList.applicationAt(newIndex);
586+ var spreadDelegate = spreadRepeater.itemAt(newIndex);
587+ if (spreadDelegate.stage === ApplicationInfoInterface.SideStage) {
565588 sideStage.showNow();
566589 }
567590 spreadView.selectedIndex = -1;
568− ApplicationManager.focusApplication(application.appId);
591+ spreadDelegate.focus = true;
569592 spreadView.phase = 0;
570593 spreadView.contentX = -spreadView.shift;
571594 }
@@ -581,7 +604,7 @@ AbstractStage {
581604 MouseArea {
582605 id: spreadRow
583606 x: spreadView.contentX
584− width: spreadView.width + Math.max(spreadView.width, ApplicationManager.count * spreadView.tileDistance)
607+ width: spreadView.width + Math.max(spreadView.width, root.topLevelSurfaceList.count * spreadView.tileDistance)
585608 height: root.height
586609
587610 onClicked: {
@@ -599,8 +622,8 @@ AbstractStage {
599622 enabled: priv.sideStageEnabled
600623
601624 onDropped: {
602− priv.setAppStage(drag.source.appId, ApplicationInfoInterface.MainStage, true);
603− ApplicationManager.focusApplication(drag.source.appId);
625+ drop.source.spreadDelegate.stage = ApplicationInfoInterface.MainStage;
626+ drop.source.spreadDelegate.focus = true;
604627 }
605628 keys: "SideStage"
606629 }
@@ -611,12 +634,12 @@ AbstractStage {
611634 height: priv.landscapeHeight
612635 x: spreadView.width - width
613636 z: {
614− if (!priv.mainStageAppId) return 0;
637+ if (!priv.mainStageItemId) return 0;
615638
616− if (priv.sideStageAppId && spreadView.nextInStack > 0) {
617− var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
639+ if (priv.sideStageItemId && spreadView.nextInStack > 0) {
640+ var nextDelegateInStack = spreadRepeater.itemAt(spreadView.nextInStack);
618641
619− if (nextAppInStack.stage === ApplicationInfoInterface.MainStage) {
642+ if (nextDelegateInStack.stage === ApplicationInfoInterface.MainStage) {
620643 // if the next app in stack is a main stage app, put the sidestage on top of it.
621644 return 2;
622645 }
@@ -631,12 +654,11 @@ AbstractStage {
631654 Behavior on opacity { UbuntuNumberAnimation {} }
632655
633656 onShownChanged: {
634− if (!shown && ApplicationManager.focusedApplicationId == priv.sideStageAppId) {
635− ApplicationManager.requestFocusApplication(priv.mainStageAppId);
636− }
637− priv.updateStageApps();
638− if (shown && priv.sideStageAppId) {
639− ApplicationManager.requestFocusApplication(priv.sideStageAppId);
657+ if (!shown && priv.sideStageDelegate && priv.focusedAppDelegate === priv.sideStageDelegate
658+ && priv.mainStageDelegate) {
659+ priv.mainStageDelegate.focus = true;
660+ } else if (shown && priv.sideStageDelegate) {
661+ priv.sideStageDelegate.focus = true;
640662 }
641663 }
642664
@@ -655,8 +677,8 @@ AbstractStage {
655677 }
656678 onDropped: {
657679 if (drop.keys == "MainStage") {
658− priv.setAppStage(drop.source.appId, ApplicationInfoInterface.SideStage, true);
659− ApplicationManager.requestFocusApplication(drop.source.appId);
680+ drop.source.spreadDelegate.stage = ApplicationInfoInterface.SideStage;
681+ drop.source.spreadDelegate.focus = true;
660682 }
661683 }
662684 drag {
@@ -669,41 +691,102 @@ AbstractStage {
669691 }
670692 }
671693
672− Repeater {
694+ TopLevelSurfaceRepeater {
673695 id: spreadRepeater
674696 objectName: "spreadRepeater"
675− model: ApplicationManager
697+ model: root.topLevelSurfaceList
698+
699+ onItemAdded: {
700+ priv.updateMainAndSideStageIndexes();
701+ if (spreadView.phase == 2) {
702+ spreadView.snapTo(index);
703+ }
704+ }
705+
706+ onItemRemoved: {
707+ priv.updateMainAndSideStageIndexes();
708+ // Unless we're closing the app ourselves in the spread,
709+ // lets make sure the spread doesn't mess up by the changing app list.
710+ if (spreadView.closingIndex == -1) {
711+ spreadView.phase = 0;
712+ spreadView.contentX = -spreadView.shift;
713+ focusTopMostApp();
714+ }
715+ }
716+ function focusTopMostApp() {
717+ if (spreadRepeater.count > 0) {
718+ var topmostDelegate = spreadRepeater.itemAt(0);
719+ topmostDelegate.focus = true;
720+ }
721+ }
676722
677723 delegate: TransformedTabletSpreadDelegate {
678724 id: spreadTile
679− objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
680− : "tabletSpreadDelegate_null";
725+ objectName: "spreadDelegate_" + model.id
726+
727+ readonly property int index: model.index
681728 width: spreadView.width
682729 height: spreadView.height
683− active: appId == priv.mainStageAppId || appId == priv.sideStageAppId
730+ active: model.id == priv.mainStageItemId || model.id == priv.sideStageItemId
684731 zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
732+ onZIndexChanged: {
733+ if (spreadView.nextInStack == model.index) {
734+ spreadView.nextZInStack = zIndex;
735+ }
736+ }
685737 selected: spreadView.selectedIndex == index
686738 otherSelected: spreadView.selectedIndex >= 0 && !selected
687− isInSideStage: priv.sideStageAppId === appId
739+ isInSideStage: priv.sideStageItemId == model.id
688740 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
689741 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
690742 maximizedAppTopMargin: root.maximizedAppTopMargin
691− dragOffset: !isDash && appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
692− application: ApplicationManager.get(index)
743+ dragOffset: !isDash && model.id == priv.mainStageItemId && root.inverseProgress > 0
744+ && spreadView.phase === 0 ? root.inverseProgress : 0
745+ application: model.application
746+ surface: model.surface
693747 closeable: !isDash
694748 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
695749
696− readonly property bool wantsMainStage: model.stage == ApplicationInfoInterface.MainStage
750+ readonly property bool wantsMainStage: stage == ApplicationInfoInterface.MainStage
751+
752+ readonly property bool isDash: model.application.appId == "unity8-dash"
697753
698− readonly property string appId: model.appId
699− readonly property bool isDash: model.appId == "unity8-dash"
754+ onFocusChanged: {
755+ if (focus && !spreadRepeater.startingUp) {
756+ priv.focusedAppDelegate = spreadTile;
757+ root.topLevelSurfaceList.raiseId(model.id);
758+ }
759+ if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
760+ sideStage.show();
761+ }
762+ }
763+ Connections {
764+ target: model.surface
765+ onFocusRequested: spreadTile.focus = true;
766+ }
767+ Connections {
768+ target: model.application
769+ onFocusRequested: {
770+ if (!model.surface) {
771+ // when an app has no surfaces, we assume there's only one entry representing it:
772+ // this delegate.
773+ spreadTile.focus = true;
774+ } else {
775+ // if the application has surfaces, focus request should be at surface-level.
776+ }
777+ }
778+ }
700779
701− stage: model.stage
702780 fullscreen: {
703− if (mainApp && stage === ApplicationInfoInterface.SideStage) {
704− return mainApp.fullscreen;
781+ if (priv.mainStageDelegate && stage === ApplicationInfoInterface.SideStage) {
782+ return priv.mainStageDelegate.fullscreen;
783+ } else if (surface) {
784+ return surface.state === Mir.FullscreenState;
785+ } else if (application) {
786+ return application.fullscreen;
787+ } else {
788+ return false;
705789 }
706− return application ? application.fullscreen : false;
707790 }
708791
709792 supportedOrientations: {
@@ -724,23 +807,6 @@ AbstractStage {
724807 }
725808 }
726809
727−
728− Binding {
729− target: spreadTile.application
730− property: "exemptFromLifecycle"
731− value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
732− }
733−
734− Binding {
735− target: spreadTile.application
736− property: "requestedState"
737− value: (isDash && root.keepDashRunning)
738− || (!root.suspended && (model.appId == priv.mainStageAppId
739− || model.appId == priv.sideStageAppId))
740− ? ApplicationInfoInterface.RequestedRunning
741− : ApplicationInfoInterface.RequestedSuspended
742− }
743−
744810 // FIXME: A regular binding doesn't update any more after closing an app.
745811 // Using a Binding for now.
746812 Binding {
@@ -760,34 +826,33 @@ AbstractStage {
760826 onSideStageEnabledChanged: refreshStage()
761827 }
762828
829+ property bool _constructing: true;
830+ onStageChanged: {
831+ if (!_constructing) {
832+ priv.updateMainAndSideStageIndexes();
833+ }
834+ }
835+
763836 Component.onCompleted: {
764− refreshStage()
765− stageChanged.connect(priv.updateStageApps);
837+ // a top level window is always the focused one when it first appears, unfocusing
838+ // any preexisting one
839+ focus = true;
840+ refreshStage();
841+ _constructing = false;
842+ }
843+ Component.onDestruction: {
844+ WindowStateStorage.saveStage(model.application.appId, stage);
766845 }
767846
768847 function refreshStage() {
769− var stage = ApplicationInfoInterface.MainStage;
848+ var newStage = ApplicationInfoInterface.MainStage;
770849 if (priv.sideStageEnabled) {
771850 if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
772− stage = WindowStateStorage.getStage(appId);
851+ newStage = WindowStateStorage.getStage(application.appId);
773852 }
774853 }
775854
776− if (model.stage !== stage) {
777− priv.setAppStage(appId, stage, false);
778− }
779− }
780−
781− // This is required because none of the bindings are triggered in some cases:
782− // When an app is closed, it might happen that ApplicationManager.get(nextInStack)
783− // returns a different app even though the nextInStackIndex and all the related
784− // bindings (index, mainStageApp, sideStageApp, etc) don't change. Let's force a
785− // binding update in that case.
786− Connections {
787− target: ApplicationManager
788− onApplicationRemoved: spreadTile.z = Qt.binding(function() {
789− return spreadView.indexToZIndex(index);
790− })
855+ stage = newStage;
791856 }
792857
793858 progress: {
@@ -873,13 +938,10 @@ AbstractStage {
873938 script: {
874939 // rotate immediately.
875940 spreadTile.matchShellOrientation();
876− if (ApplicationManager.focusedApplicationId === spreadTile.appId &&
941+ if (priv.focusedAppDelegate === spreadTile &&
877942 priv.sideStageEnabled && !sideStage.shown) {
878943 // Sidestage was focused, so show the side stage.
879944 sideStage.show();
880− // if we've switched to a main app which doesnt support portrait, hide the side stage.
881− } else if (mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
882− sideStage.hideNow();
883945 }
884946 }
885947 }
@@ -890,10 +952,10 @@ AbstractStage {
890952 SequentialAnimation {
891953 ScriptAction {
892954 script: {
893− if (priv.sideStageAppId === spreadTile.appId &&
955+ if (priv.sideStageDelegate === spreadTile &&
894956 mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
895957 // The mainstage app did not natively support portrait orientation, so focus the sidestage.
896− ApplicationManager.requestFocusApplication(spreadTile.appId);
958+ spreadTile.focus = true;
897959 }
898960 }
899961 }
@@ -922,28 +984,26 @@ AbstractStage {
922984
923985 onClosed: {
924986 spreadView.closingIndex = index;
925− ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
926− }
927−
928− onFocusChanged: {
929− if (focus && ApplicationManager.focusedApplicationId !== appId) {
930− ApplicationManager.focusApplication(appId);
931− }
932−
933− if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
934− sideStage.show();
987+ if (spreadTile.surface) {
988+ spreadTile.surface.close();
989+ } else if (spreadTile.application) {
990+ root.applicationManager.stopApplication(spreadTile.application.appId);
991+ } else {
992+ // should never happen
993+ console.warn("Can't close topLevelSurfaceList entry as it has neither"
994+ + " a surface nor an application");
935995 }
936996 }
937997
938998 Binding {
939999 target: root
940− when: model.appId == priv.mainStageAppId
1000+ when: model.id == priv.mainStageItemId
9411001 property: "mainAppWindowOrientationAngle"
9421002 value: appWindowOrientationAngle
9431003 }
9441004 Binding {
9451005 target: priv
946− when: model.appId == priv.mainStageAppId
1006+ when: model.id == priv.mainStageItemId
9471007 property: "mainAppOrientationChangesEnabled"
9481008 value: orientationChangesEnabled
9491009 }
@@ -957,7 +1017,7 @@ AbstractStage {
9571017
9581018 StagedFullscreenPolicy {
9591019 id: fullscreenPolicy
960− application: spreadTile.application
1020+ surface: model.surface
9611021 }
9621022 Connections {
9631023 target: root
@@ -973,17 +1033,19 @@ AbstractStage {
9731033 anchors.fill: parent
9741034 enabled: priv.sideStageEnabled && !spreadView.active
9751035 property var dragObject: null
976− property string appId: ""
1036+
1037+ property Item spreadDelegate
1038+
9771039 dragComponent: dragComponent
978− dragComponentProperties: { "appId": appId }
1040+ dragComponentProperties: { "spreadDelegate": spreadDelegate }
9791041
9801042 onPressed: {
981− function matchDelegate(obj) { return String(obj.objectName).indexOf("tabletSpreadDelegate") >= 0; }
1043+ function matchDelegate(obj) { return String(obj.objectName).indexOf("spreadDelegate") >= 0; }
9821044
9831045 var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
9841046 if (!delegateAtCenter) return;
9851047
986− appId = delegateAtCenter.appId;
1048+ spreadDelegate = delegateAtCenter;
9871049 }
9881050
9891051 onClicked: {
@@ -1003,11 +1065,11 @@ AbstractStage {
10031065
10041066 Component {
10051067 id: dragComponent
1006− SessionContainer {
1007− property string appId: ""
1008− property var application: ApplicationManager.findApplication(appId)
1068+ SurfaceContainer {
1069+ property Item spreadDelegate
1070+
1071+ surface: spreadDelegate ? spreadDelegate.surface : null
10091072
1010− session: application ? application.session : null
10111073 interactive: false
10121074 resizeSurface: false
10131075 focus: false
@@ -1019,10 +1081,11 @@ AbstractStage {
10191081 Drag.hotSpot.y: height/2
10201082 // only accept opposite stage.
10211083 Drag.keys: {
1022− if (!application) return "Disabled";
1084+ if (!surface) return "Disabled";
10231085
1024− if (application.stage === ApplicationInfo.MainStage) {
1025− if (application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1086+ if (spreadDelegate.stage === ApplicationInfo.MainStage) {
1087+ if (spreadDelegate.application.supportedOrientations
1088+ & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
10261089 return "MainStage";
10271090 }
10281091 return "Disabled";
qml/Stages/TopLevelSurfaceRepeater.qmlnew file+52
@@ -0,0 +1,52 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+
19+Repeater {
20+ id: root
21+ // FIXME: This is a hack around us not knowing whether the Repeater has finished creating its
22+ // delegates on start up.
23+ // This is a problem when the stage gets a TopLevelSurfaceList already populated with several
24+ // rows.
25+ property bool startingUp: true
26+ onStartingUpChanged: {
27+ if (!startingUp) {
28+ // the top-most surface must be the focused one.
29+ var topmostDelegate = itemAt(0);
30+ topmostDelegate.focus = true;
31+ }
32+ }
33+
34+ onItemAdded: {
35+ if (startingUp) {
36+ checkIfStillStartingUp();
37+ }
38+ }
39+
40+ function checkIfStillStartingUp() {
41+ var i = 0;
42+ var missingDelegate = false;
43+ for (i = 0; i < model.count && !missingDelegate; ++i) {
44+ if (!itemAt(i)) {
45+ missingDelegate = true;
46+ }
47+ }
48+ if (!missingDelegate) {
49+ startingUp = false;
50+ }
51+ }
52+}
qml/Stages/TransformedSpreadDelegate.qml+4−4
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014,2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU Lesser General Public License as published by
@@ -312,7 +312,7 @@ SpreadDelegate {
312312 // non-fullscreen window when they're stacked on top of each other on the
313313 // far left of the spread.
314314 Translate {
315− y: !fullscreen && appWindowRotation === 180
315+ y: !root.fullscreen && appWindowRotation === 180
316316 ? priv.topMarginProgress * maximizedAppTopMargin
317317 : 0
318318 },
@@ -326,7 +326,7 @@ SpreadDelegate {
326326 switch (appWindowRotation) {
327327 case 90:
328328 case 270:
329− if (fullscreen) {
329+ if (root.fullscreen) {
330330 return 1;
331331 } else {
332332 return 1 + priv.topMarginProgress * maximizedAppTopMargin / spreadView.width;
@@ -340,7 +340,7 @@ SpreadDelegate {
340340 yScale: {
341341 switch (appWindowRotation) {
342342 case 0:
343− if (fullscreen) {
343+ if (root.fullscreen) {
344344 return 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height;
345345 } else {
346346 return 1;
qml/Stages/TransformedTabletSpreadDelegate.qml+29−23
@@ -31,7 +31,6 @@ SpreadDelegate {
3131 // Set this to true when this tile a currently active on either the MS or the SS.
3232 property bool active: false
3333
34− property int stage
3534 property int zIndex
3635 property real progress: 0
3736 property real animatedProgress: 0
@@ -48,13 +47,15 @@ SpreadDelegate {
4847
4948 property bool isInSideStage: false
5049
50+ property int stage: ApplicationInfoInterface.MainStage
51+
5152 property int dragOffset: 0
5253 readonly property alias xTranslateAnimating: xTranslateAnimation.running
5354 readonly property bool offScreen: priv.xTranslate >= 0
5455
5556 dropShadow: spreadView.active ||
5657 (active
57− && (stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
58+ && (root.stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
5859 && priv.xTranslate != 0)
5960
6061 onSelectedChanged: {
@@ -90,7 +91,7 @@ SpreadDelegate {
9091 id: priv
9192
9293 // true if this is the next tile on the stack that comes in when dragging from the right
93− property bool nextInStack: spreadView.nextZInStack == zIndex
94+ property bool nextInStack: spreadView.nextInStack == model.index
9495 // true if the next tile in the stack is the nextInStack one. This one will be moved a bit to the left
9596 property bool movedActive: spreadView.nextZInStack == zIndex + 1
9697 property real animatedEndDistance: linearAnimation(0, 2, root.endDistance, 0, root.progress)
@@ -142,7 +143,7 @@ SpreadDelegate {
142143 Behavior on xTranslate {
143144 enabled: !spreadView.active &&
144145 !snapAnimation.running &&
145− model.appId !== "unity8-dash" &&
146+ root.application.appId !== "unity8-dash" &&
146147 spreadView.animateX &&
147148 !spreadView.beingResized &&
148149 priv.state !== "sideStage"
@@ -156,8 +157,9 @@ SpreadDelegate {
156157 var newTranslate = 0;
157158
158159 // selected app or opposite stage active app.
159− if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
160− if (stage == ApplicationInfoInterface.MainStage) {
160+ if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
161+ && spreadView.selectedDelegate.stage !== root.stage)) {
162+ if (root.stage == ApplicationInfoInterface.MainStage) {
161163 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
162164 } else {
163165 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
@@ -168,8 +170,8 @@ SpreadDelegate {
168170
169171 // The tile should move a bit to the left if a new one comes on top of it, but not for the Side Stage and not
170172 // when we're only dragging the side stage in on top of a main stage app
171− var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && stage === ApplicationInfoInterface.MainStage &&
172− ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
173+ var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && root.stage === ApplicationInfoInterface.MainStage &&
174+ topLevelSurfaceList.applicationAt(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
173175
174176 if (active) {
175177 newTranslate -= root.width
@@ -178,7 +180,7 @@ SpreadDelegate {
178180 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
179181 }
180182 newTranslate += root.dragOffset;
181− } else if (!spreadView.active && model.appId == "unity8-dash") {
183+ } else if (!spreadView.active && root.application.appId == "unity8-dash") {
182184 newTranslate -= root.width;
183185 }
184186
@@ -190,14 +192,14 @@ SpreadDelegate {
190192
191193 if (spreadView.phase == 0) {
192194 if (nextInStack) {
193− if (stage == ApplicationInfoInterface.MainStage) {
195+ if (root.stage == ApplicationInfoInterface.MainStage) {
194196 if (spreadView.sideStageVisible && root.progress > 0) {
195197 // Move it so it appears from behind the side stage immediately
196198 newTranslate += -spreadView.sideStageWidth;
197199 }
198200 }
199201
200− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
202+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
201203 // This is when we only drag the side stage in, without rotation or snapping
202204 newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
203205 } else {
@@ -210,7 +212,7 @@ SpreadDelegate {
210212
211213 if (spreadView.phase == 1) {
212214 if (nextInStack) {
213− if (stage == ApplicationInfoInterface.MainStage) {
215+ if (root.stage == ApplicationInfoInterface.MainStage) {
214216 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
215217 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
216218 } else {
@@ -250,7 +252,8 @@ SpreadDelegate {
250252 }
251253
252254 // selected app or opposite stage active app.
253− if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
255+ if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
256+ && spreadView.selectedDelegate.stage !== root.stage)) {
254257 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
255258 } else if (otherSelected) {
256259 return selectedScale;
@@ -258,7 +261,7 @@ SpreadDelegate {
258261
259262 if (spreadView.phase == 0) {
260263 if (nextInStack) {
261− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
264+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
262265 return 1;
263266 } else {
264267 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
@@ -272,7 +275,7 @@ SpreadDelegate {
272275 if (spreadView.phase == 1) {
273276 if (nextInStack) {
274277 var startScale = 1;
275− if (stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
278+ if (root.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
276279 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
277280 }
278281 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
@@ -295,7 +298,8 @@ SpreadDelegate {
295298 }
296299
297300 // selected app or opposite stage active app.
298− if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
301+ if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
302+ && spreadView.selectedDelegate.stage !== root.stage)) {
299303 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
300304 } else if (otherSelected) {
301305 return selectedAngle;
@@ -304,12 +308,12 @@ SpreadDelegate {
304308 // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
305309 var shouldMoveAway = spreadView.nextInStack == -1 ||
306310 spreadView.nextInStack >= 0 && priv.movedActive &&
307− (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
308− stage == ApplicationInfoInterface.SideStage);
311+ (topLevelSurfaceList.applicationAt(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
312+ root.stage == ApplicationInfoInterface.SideStage);
309313
310314 if (spreadView.phase == 0) {
311315 if (nextInStack) {
312− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
316+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
313317 return 0;
314318 } else {
315319 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
@@ -321,7 +325,7 @@ SpreadDelegate {
321325 }
322326 if (spreadView.phase == 1) {
323327 if (nextInStack) {
324− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
328+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
325329 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
326330 } else {
327331 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
@@ -345,7 +349,8 @@ SpreadDelegate {
345349 if (root.isSelected) return 1;
346350
347351 if (otherSelected) {
348− if (root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage) {
352+ if (root.active && spreadView.selectedDelegate
353+ && spreadView.selectedDelegate.stage !== root.stage) {
349354 return 1;
350355 }
351356 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
@@ -355,7 +360,8 @@ SpreadDelegate {
355360
356361 property real topMarginProgress: {
357362 // selected app or opposite stage active app.
358− if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
363+ if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
364+ && spreadView.selectedDelegate.stage !== root.stage)) {
359365 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
360366 }
361367
@@ -379,7 +385,7 @@ SpreadDelegate {
379385 when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
380386 PropertyChanges {
381387 target: priv;
382− xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-spreadView.sideStageDragProgress)
388+ xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-sideStage.progress)
383389 }
384390 }
385391 ]
qml/Stages/WindowedFullscreenPolicy.qml+5−7
@@ -23,19 +23,17 @@ import Unity.Application 0.1
2323// state of the window is returned to restored.
2424QtObject {
2525 property bool active: true
26− property QtObject application: null
26+ property QtObject surface: null
2727
28− readonly property var lastSurface: application && application.session ?
29− application.session.lastSurface : null
3028 property bool _firstTimeSurface: true
3129
32− onLastSurfaceChanged: {
33− if (!active || !lastSurface) return;
30+ onSurfaceChanged: {
31+ if (!active || !surface) return;
3432 if (!_firstTimeSurface) return;
3533 _firstTimeSurface = false;
3634
37− if (lastSurface.state === Mir.FullscreenState && lastSurface.shellChrome === Mir.LowChrome) {
38− lastSurface.state = Mir.RestoredState;
35+ if (surface.state === Mir.FullscreenState && surface.shellChrome === Mir.LowChrome) {
36+ surface.state = Mir.RestoredState;
3937 }
4038 }
4139}
qml/Tutorial/TutorialBottom.qml+1−1
@@ -47,7 +47,7 @@ TutorialPage {
4747 readonly property real sideStageWidth: root.usageScenario === "tablet" && stage.sideStageVisible ?
4848 stage.sideStageWidth : 0
4949 readonly property bool isMainStageApp: usageScenario !== "tablet" ||
50− application.stage === ApplicationInfoInterface.MainStage
50+ stage.stageFocusedSurface === ApplicationInfoInterface.MainStage
5151 readonly property real dragAreaHeight: units.gu(3) // based on PageWithBottomEdge.qml
5252 readonly property real targetDistance: height * 0.2 + dragAreaHeight // based on PageWithBottomEdge.qml
5353
tests/mocks/Unity/Application/ApplicationInfo.cpp+181−108
@@ -16,8 +16,7 @@
1616
1717#include "ApplicationInfo.h"
1818#include "MirSurface.h"
19−#include "Session.h"
20−#include "SessionManager.h"
19+#include "SurfaceManager.h"
2120
2221#include <paths.h>
2322
@@ -25,97 +24,114 @@
2524#include <QQuickItem>
2625#include <QQuickView>
2726#include <QQmlComponent>
28−#include <QTimer>
27+
28+#define APPLICATION_DEBUG 0
29+
30+#if APPLICATION_DEBUG
31+#define DEBUG_MSG(params) qDebug().nospace() << "Application["<<appId()<<"]::" << __func__ << " " << params
32+
33+QString stateToStr(ApplicationInfo::State state)
34+{
35+ switch (state) {
36+ case ApplicationInfo::Starting:
37+ return "starting";
38+ case ApplicationInfo::Running:
39+ return "running";
40+ case ApplicationInfo::Suspended:
41+ return "suspended";
42+ case ApplicationInfo::Stopped:
43+ return "stopped";
44+ default:
45+ return "???";
46+ };
47+}
48+
49+#else
50+#define DEBUG_MSG(params) ((void)0)
51+#endif
52+
53+#define WARNING_MSG(params) qWarning().nospace() << "Application["<<appId()<<"]::" << __func__ << " " << params
2954
3055ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3156 : ApplicationInfoInterface(appId, parent)
3257 , m_appId(appId)
33− , m_stage(MainStage)
34− , m_state(Stopped)
35− , m_focused(false)
36− , m_fullscreen(false)
37− , m_session(0)
38− , m_supportedOrientations(Qt::PortraitOrientation |
39− Qt::LandscapeOrientation |
40− Qt::InvertedPortraitOrientation |
41− Qt::InvertedLandscapeOrientation)
42− , m_rotatesWindowContents(false)
43− , m_requestedState(RequestedRunning)
44− , m_isTouchApp(true)
45− , m_exemptFromLifecycle(false)
46− , m_manualSurfaceCreation(false)
47− , m_shellChrome(Mir::NormalChrome)
4858{
59+ connect(&m_surfaceList, &MirSurfaceListModel::countChanged,
60+ this, &ApplicationInfo::onSurfaceCountChanged, Qt::QueuedConnection);
61+
62+ m_surfaceCreationTimer.setSingleShot(true);
63+ m_surfaceCreationTimer.setInterval(500);
64+ connect(&m_surfaceCreationTimer, &QTimer::timeout, this, &ApplicationInfo::createSurface);
4965}
5066
5167ApplicationInfo::ApplicationInfo(QObject *parent)
52− : ApplicationInfoInterface(QString(), parent)
53− , m_stage(MainStage)
54− , m_state(Stopped)
55− , m_focused(false)
56− , m_fullscreen(false)
57− , m_session(0)
58− , m_supportedOrientations(Qt::PortraitOrientation |
59− Qt::LandscapeOrientation |
60− Qt::InvertedPortraitOrientation |
61− Qt::InvertedLandscapeOrientation)
62− , m_rotatesWindowContents(false)
63− , m_requestedState(RequestedRunning)
64− , m_isTouchApp(true)
65− , m_exemptFromLifecycle(false)
66− , m_manualSurfaceCreation(false)
67− , m_shellChrome(Mir::NormalChrome)
68+ : ApplicationInfo(QString(), parent)
6869{
6970}
7071
7172ApplicationInfo::~ApplicationInfo()
7273{
73− delete m_session;
7474}
7575
76−void ApplicationInfo::createSession()
76+void ApplicationInfo::createSurface()
7777{
78− if (m_session || state() == ApplicationInfo::Stopped) { return; }
79−
80− setSession(SessionManager::singleton()->createSession(appId(), m_screenshotFileName));
81−}
78+ if (state() == ApplicationInfo::Stopped) { return; }
8279
83−void ApplicationInfo::destroySession()
84−{
85− Session *session = this->session();
86− setSession(nullptr);
87− delete session;
88−}
80+ QString surfaceName = name();
81+ if (m_surfaceList.count() > 0) {
82+ surfaceName.append(QString(" %1").arg(m_surfaceList.count()+1));
83+ }
8984
90−void ApplicationInfo::setSession(Session* session)
91−{
92− if (m_session == session)
85+ auto surfaceManager = SurfaceManager::instance();
86+ if (!surfaceManager) {
87+ WARNING_MSG("No SurfaceManager");
9388 return;
94−
95− if (m_session) {
96− disconnect(this, 0, m_session, 0);
97− m_session->setApplication(nullptr);
98− m_session->setParent(nullptr);
99− Q_EMIT m_session->deregister();
10089 }
10190
102− m_session = session;
103−
104− if (m_session) {
105− m_session->setApplication(this);
106− m_session->setParent(this);
107− m_session->setFullscreen(m_fullscreen);
108− SessionManager::singleton()->registerSession(m_session);
109− connect(m_session, &Session::surfaceAdded,
110− this, &ApplicationInfo::onSessionSurfaceAdded);
111− connect(m_session, &Session::fullscreenChanged, this, &ApplicationInfo::fullscreenChanged);
91+ auto surface = surfaceManager->createSurface(surfaceName,
92+ Mir::NormalType,
93+ fullscreen() ? Mir::FullscreenState : Mir::MaximizedState,
94+ m_screenshotFileName);
95+
96+ surface->setShellChrome(m_shellChrome);
97+
98+ m_surfaceList.appendSurface(surface);
99+
100+ ++m_liveSurfaceCount;
101+ connect(surface, &MirSurface::liveChanged, this, [this, surface](){
102+ if (!surface->live()) {
103+ --m_liveSurfaceCount;
104+ if (m_liveSurfaceCount == 0) {
105+ if (m_closingSurfaces.contains(surface)
106+ || (m_state == Running && m_requestedState == RequestedRunning)) {
107+ Q_EMIT closed();
108+ }
109+ setState(Stopped);
110+ } else {
111+ if (m_closingSurfaces.contains(surface) && m_requestedState == RequestedSuspended
112+ && m_closingSurfaces.count() == 1) {
113+ setState(Suspended);
114+ }
115+ }
116+ m_closingSurfaces.removeAll(surface);
117+ }
118+ });
119+ connect(surface, &MirSurface::closeRequested, this, [this, surface](){
120+ m_closingSurfaces.append(surface);
121+ if (m_state == Suspended) {
122+ // resume to allow application to close its surface
123+ setState(Running);
124+ }
125+ });
126+ connect(surface, &MirSurface::focusRequested, this, &ApplicationInfo::focusRequested);
112127
113− if (!m_manualSurfaceCreation) {
114− QTimer::singleShot(500, m_session, &Session::createSurface);
128+ if (m_state == Starting) {
129+ if (m_requestedState == RequestedRunning) {
130+ setState(Running);
131+ } else {
132+ setState(Suspended);
115133 }
116134 }
117−
118− Q_EMIT sessionChanged(m_session);
119135}
120136
121137void ApplicationInfo::setIconId(const QString &iconId)
@@ -138,10 +154,6 @@ void ApplicationInfo::setScreenshotId(const QString &screenshotId)
138154
139155 if (screenshotFileName != m_screenshotFileName) {
140156 m_screenshotFileName = screenshotFileName;
141−
142− if (m_session) {
143− m_session->setScreenshot(screenshotFileName);
144− }
145157 }
146158}
147159
@@ -172,38 +184,53 @@ void ApplicationInfo::setStage(Stage value)
172184void ApplicationInfo::setState(State value)
173185{
174186 if (value != m_state) {
187+ DEBUG_MSG(qPrintable(stateToStr(value)));
188+ if (!m_manualSurfaceCreation && value == ApplicationInfo::Starting) {
189+ Q_ASSERT(m_surfaceList.count() == 0);
190+ m_surfaceCreationTimer.start();
191+ } else if (value == ApplicationInfo::Stopped) {
192+ m_surfaceCreationTimer.stop();
193+ for (int i = 0; i < m_surfaceList.count(); ++i) {
194+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
195+ surface->setLive(false);
196+ }
197+ }
198+
175199 m_state = value;
176200 Q_EMIT stateChanged(value);
177−
178− if (!m_manualSurfaceCreation && !m_session && m_state == ApplicationInfo::Starting) {
179− QTimer::singleShot(500, this, &ApplicationInfo::createSession);
180− } else if (m_state == ApplicationInfo::Stopped) {
181− Session *session = m_session;
182− setSession(nullptr);
183− delete session;
184− }
185201 }
186202}
187203
188−void ApplicationInfo::setFocused(bool value)
204+void ApplicationInfo::close()
189205{
190− if (value != m_focused) {
191− m_focused = value;
192− Q_EMIT focusedChanged(value);
206+ DEBUG_MSG("");
207+
208+ if (m_surfaceList.count() > 0) {
209+ for (int i = 0; i < m_surfaceList.count(); ++i) {
210+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
211+ surface->close();
212+ }
213+ } else {
214+ setState(Stopped);
215+ Q_EMIT closed();
193216 }
194217}
195218
196219void ApplicationInfo::setFullscreen(bool value)
197220{
198221 m_fullscreen = value;
199− if (m_session) {
200− m_session->setFullscreen(value);
222+ if (m_surfaceList.rowCount() > 0) {
223+ m_surfaceList.get(0)->setState(Mir::FullscreenState);
201224 }
202225}
203226
204227bool ApplicationInfo::fullscreen() const
205228{
206− return m_session ? m_session->fullscreen() : m_fullscreen;
229+ if (m_surfaceList.rowCount() > 0) {
230+ return m_surfaceList.get(0)->state() == Mir::FullscreenState;
231+ } else {
232+ return m_fullscreen;
233+ }
207234}
208235
209236void ApplicationInfo::setManualSurfaceCreation(bool value)
@@ -241,15 +268,28 @@ ApplicationInfo::RequestedState ApplicationInfo::requestedState() const
241268
242269void ApplicationInfo::setRequestedState(RequestedState value)
243270{
244− if (m_requestedState != value) {
245− m_requestedState = value;
246− Q_EMIT requestedStateChanged(m_requestedState);
271+ if (m_requestedState == value) {
272+ return;
273+ }
274+ DEBUG_MSG((value == RequestedRunning ? "RequestedRunning" : "RequestedSuspended") );
247275
248− if (m_requestedState == RequestedRunning && m_state == Suspended) {
276+ m_requestedState = value;
277+ Q_EMIT requestedStateChanged(m_requestedState);
278+
279+ if (m_requestedState == RequestedRunning) {
280+
281+ if (m_state == Suspended) {
282+ Q_ASSERT(m_liveSurfaceCount > 0);
249283 setState(Running);
250− } else if (m_requestedState == RequestedSuspended && m_state == Running) {
251− setState(Suspended);
284+ } else if (m_state == Stopped) {
285+ Q_ASSERT(m_liveSurfaceCount == 0);
286+ // it's restarting
287+ setState(Starting);
252288 }
289+
290+ } else if (m_requestedState == RequestedSuspended && m_state == Running
291+ && m_closingSurfaces.isEmpty()) {
292+ setState(Suspended);
253293 }
254294}
255295
@@ -263,18 +303,6 @@ void ApplicationInfo::setIsTouchApp(bool isTouchApp)
263303 m_isTouchApp = isTouchApp;
264304}
265305
266−void ApplicationInfo::onSessionSurfaceAdded(MirSurface* surface)
267−{
268− if (surface != nullptr && m_state == Starting) {
269− if (m_requestedState == RequestedRunning) {
270− setState(Running);
271− } else {
272− setState(Suspended);
273− }
274− surface->setShellChrome(m_shellChrome);
275− }
276−}
277−
278306bool ApplicationInfo::exemptFromLifecycle() const
279307{
280308 return m_exemptFromLifecycle;
@@ -305,7 +333,52 @@ void ApplicationInfo::setInitialSurfaceSize(const QSize &size)
305333void ApplicationInfo::setShellChrome(Mir::ShellChrome shellChrome)
306334{
307335 m_shellChrome = shellChrome;
308− if (m_session && m_session->lastSurface()) {
309− m_session->lastSurface()->setShellChrome(shellChrome);
336+ if (m_surfaceList.rowCount() > 0) {
337+ static_cast<MirSurface*>(m_surfaceList.get(0))->setShellChrome(shellChrome);
338+ }
339+}
340+
341+bool ApplicationInfo::focused() const
342+{
343+ bool someSurfaceHasFocus = false; // to be proven wrong
344+ for (int i = 0; i < m_surfaceList.count() && !someSurfaceHasFocus; ++i) {
345+ someSurfaceHasFocus = m_surfaceList.get(i)->focused();
346+ }
347+ return someSurfaceHasFocus;
348+}
349+
350+void ApplicationInfo::setFocused(bool value)
351+{
352+ if (focused() == value) {
353+ return;
354+ }
355+
356+ if (value) {
357+ if (m_surfaceList.count() > 0) {
358+ m_surfaceList.get(0)->requestFocus();
359+ }
360+ } else {
361+ for (int i = 0; i < m_surfaceList.count(); ++i) {
362+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
363+ if (surface->focused()) {
364+ surface->setFocused(false);
365+ }
366+ }
367+ }
368+}
369+
370+void ApplicationInfo::onSurfaceCountChanged()
371+{
372+ if (m_surfaceList.count() == 0 && m_state == Running) {
373+ setState(Stopped);
374+ }
375+}
376+
377+void ApplicationInfo::requestFocus()
378+{
379+ if (m_surfaceList.count() == 0) {
380+ Q_EMIT focusRequested();
381+ } else {
382+ m_surfaceList.get(0)->requestFocus();
310383 }
311384}
tests/mocks/Unity/Application/ApplicationInfo.h+39−24
@@ -20,25 +20,32 @@
2020#include <QObject>
2121
2222class MirSurface;
23−class Session;
2423
2524// unity-api
2625#include <unity/shell/application/ApplicationInfoInterface.h>
2726#include <unity/shell/application/Mir.h>
2827
28+#include "MirSurfaceListModel.h"
29+
30+#include <QList>
31+#include <QTimer>
32+
2933using namespace unity::shell::application;
3034
3135class ApplicationInfo : public ApplicationInfoInterface {
3236 Q_OBJECT
3337
38+ ////
39+ // FIXME: Remove those
3440 Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged)
35− Q_PROPERTY(Session* session READ session NOTIFY sessionChanged)
3641
3742 // Only exists in this fake implementation
3843
3944 // whether the test code will explicitly control the creation of the application surface
4045 Q_PROPERTY(bool manualSurfaceCreation READ manualSurfaceCreation WRITE setManualSurfaceCreation NOTIFY manualSurfaceCreationChanged)
4146
47+ Q_PROPERTY(QString screenshot READ screenshot CONSTANT)
48+
4249public:
4350 ApplicationInfo(QObject *parent = nullptr);
4451 ApplicationInfo(const QString &appId, QObject *parent = nullptr);
@@ -66,8 +73,7 @@ public:
6673 Q_INVOKABLE void setState(State value);
6774 State state() const override { return m_state; }
6875
69− void setFocused(bool value);
70− bool focused() const override { return m_focused; }
76+ bool focused() const override;
7177
7278 QString splashTitle() const override { return QString(); }
7379 QUrl splashImage() const override { return QUrl(); }
@@ -100,21 +106,26 @@ public:
100106 void setInitialSurfaceSize(const QSize &size) override;
101107
102108 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
103−public:
104− void setSession(Session* session);
105− Session* session() const { return m_session; }
109+
110+ MirSurfaceListInterface* surfaceList() override { return &m_surfaceList; }
111+
112+ void setFocused(bool value);
113+
114+ //////
115+ // internal mock stuff
116+ void close();
117+ void requestFocus();
106118
107119Q_SIGNALS:
108− void sessionChanged(Session*);
109120 void fullscreenChanged(bool value);
110121 void manualSurfaceCreationChanged(bool value);
122+ void closed();
111123
112124public Q_SLOTS:
113− Q_INVOKABLE void createSession();
114− Q_INVOKABLE void destroySession();
125+ Q_INVOKABLE void createSurface();
115126
116127private Q_SLOTS:
117− void onSessionSurfaceAdded(MirSurface*);
128+ void onSurfaceCountChanged();
118129
119130private:
120131 void setIcon(const QUrl &value);
@@ -124,20 +135,24 @@ private:
124135 QString m_appId;
125136 QString m_name;
126137 QUrl m_icon;
127− Stage m_stage;
128− State m_state;
129− bool m_focused;
130− bool m_fullscreen;
131− Session* m_session;
132− Qt::ScreenOrientations m_supportedOrientations;
133− bool m_rotatesWindowContents;
134− RequestedState m_requestedState;
135− bool m_isTouchApp;
136− bool m_exemptFromLifecycle;
138+ Stage m_stage{MainStage};
139+ State m_state{Stopped};
140+ bool m_fullscreen{false};
141+ Qt::ScreenOrientations m_supportedOrientations{Qt::PortraitOrientation |
142+ Qt::LandscapeOrientation |
143+ Qt::InvertedPortraitOrientation |
144+ Qt::InvertedLandscapeOrientation};
145+ bool m_rotatesWindowContents{false};
146+ RequestedState m_requestedState{RequestedRunning};
147+ bool m_isTouchApp{true};
148+ bool m_exemptFromLifecycle{false};
137149 QSize m_initialSurfaceSize;
138−
139− bool m_manualSurfaceCreation;
140− Mir::ShellChrome m_shellChrome;
150+ MirSurfaceListModel m_surfaceList;
151+ int m_liveSurfaceCount{0};
152+ QTimer m_surfaceCreationTimer;
153+ QList<MirSurface*> m_closingSurfaces;
154+ bool m_manualSurfaceCreation{false};
155+ Mir::ShellChrome m_shellChrome{Mir::NormalChrome};
141156};
142157
143158Q_DECLARE_METATYPE(ApplicationInfo*)
tests/mocks/Unity/Application/ApplicationManager.cpp+109−74
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013-2015 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,7 @@
1616
1717#include "ApplicationManager.h"
1818#include "ApplicationInfo.h"
19−#include "Session.h"
20−#include "ApplicationTestInterface.h"
19+#include "MirSurface.h"
2120
2221#include <paths.h>
2322#include <csignal>
@@ -31,28 +30,20 @@
3130#include <QDateTime>
3231#include <QtDBus/QtDBus>
3332
34−ApplicationManager *ApplicationManager::the_application_manager = nullptr;
33+#define APPLICATIONMANAGER_DEBUG 0
3534
36−ApplicationManager *ApplicationManager::singleton()
37−{
38− if (!the_application_manager) {
39− the_application_manager = new ApplicationManager();
35+#if APPLICATIONMANAGER_DEBUG
36+#define DEBUG_MSG(params) qDebug().nospace() << "ApplicationManager::" << __func__ << " " << params
37+#else
38+#define DEBUG_MSG(params) ((void)0)
39+#endif
4040
41− // Emit signal to notify Upstart that Mir is ready to receive client connections
42− // see http://upstart.ubuntu.com/cookbook/#expect-stop
43− if (qgetenv("UNITY_MIR_EMITS_SIGSTOP") == "1") {
44− raise(SIGSTOP);
45− }
46− }
47− return the_application_manager;
48−}
41+namespace unityapi = unity::shell::application;
4942
5043ApplicationManager::ApplicationManager(QObject *parent)
5144 : ApplicationManagerInterface(parent)
5245{
53− m_roleNames.insert(RoleSession, "session");
54− m_roleNames.insert(RoleFullscreen, "fullscreen");
55−
46+ DEBUG_MSG("");
5647 buildListOfAvailableApplications();
5748
5849 // polling to find out when the toplevel window has been created as there's
@@ -61,6 +52,19 @@ ApplicationManager::ApplicationManager(QObject *parent)
6152 this, &ApplicationManager::onWindowCreatedTimerTimeout);
6253 m_windowCreatedTimer.setSingleShot(false);
6354 m_windowCreatedTimer.start(200);
55+
56+ Q_ASSERT(MirFocusController::instance());
57+ connect(MirFocusController::instance(), &MirFocusController::focusedSurfaceChanged,
58+ this, &ApplicationManager::updateFocusedApplication, Qt::QueuedConnection);
59+
60+
61+ // Emit signal to notify Upstart that Mir is ready to receive client connections
62+ // see http://upstart.ubuntu.com/cookbook/#expect-stop
63+ // We do this because some autopilot tests actually use this mock Unity.Application module,
64+ // so we have to mimic what the real ApplicationManager does in that regard.
65+ if (qgetenv("UNITY_MIR_EMITS_SIGSTOP") == "1") {
66+ raise(SIGSTOP);
67+ }
6468}
6569
6670ApplicationManager::~ApplicationManager()
@@ -78,7 +82,6 @@ void ApplicationManager::onWindowCreatedTimerTimeout()
7882void ApplicationManager::onWindowCreated()
7983{
8084 startApplication("unity8-dash");
81− focusApplication("unity8-dash");
8285}
8386
8487int ApplicationManager::rowCount(const QModelIndex& parent) const {
@@ -109,10 +112,8 @@ QVariant ApplicationManager::data(const QModelIndex& index, int role) const {
109112 return app->isTouchApp();
110113 case RoleExemptFromLifecycle:
111114 return app->exemptFromLifecycle();
112− case RoleSession:
113− return QVariant::fromValue(app->session());
114− case RoleFullscreen:
115− return app->fullscreen();
115+ case RoleApplication:
116+ return QVariant::fromValue(static_cast<unityapi::ApplicationInfoInterface*>(app));
116117 default:
117118 return QVariant();
118119 }
@@ -144,19 +145,17 @@ QModelIndex ApplicationManager::findIndex(ApplicationInfo* application)
144145 return QModelIndex();
145146}
146147
147−void ApplicationManager::add(ApplicationInfo *application) {
148+bool ApplicationManager::add(ApplicationInfo *application) {
148149 if (!application || m_runningApplications.contains(application)) {
149− return;
150+ return false;
150151 }
152+ DEBUG_MSG(application->appId());
153+
154+ application->setState(ApplicationInfo::Starting);
151155
152156 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
153157 m_runningApplications.append(application);
154158
155− connect(application, &ApplicationInfo::sessionChanged, this, [application, this]() {
156− QModelIndex appIndex = findIndex(application);
157− if (!appIndex.isValid()) return;
158− Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleSession);
159− });
160159 connect(application, &ApplicationInfo::focusedChanged, this, [application, this]() {
161160 QModelIndex appIndex = findIndex(application);
162161 if (!appIndex.isValid()) return;
@@ -173,19 +172,27 @@ void ApplicationManager::add(ApplicationInfo *application) {
173172 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleStage);
174173 });
175174
175+ connect(application, &ApplicationInfo::closed, this, [application, this]() {
176+ this->remove(application);
177+ });
178+ connect(application, &ApplicationInfo::focusRequested, this, [application, this]() {
179+ Q_EMIT this->focusRequested(application->appId());
180+ });
181+
176182 endInsertRows();
177− Q_EMIT applicationAdded(application->appId());
178183 Q_EMIT countChanged();
179184 if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
185+
186+ return true;
180187}
181188
182189void ApplicationManager::remove(ApplicationInfo *application) {
183190 int i = m_runningApplications.indexOf(application);
184191 if (i != -1) {
192+ DEBUG_MSG(application->appId());
185193 beginRemoveRows(QModelIndex(), i, i);
186194 m_runningApplications.removeAt(i);
187195 endRemoveRows();
188− Q_EMIT applicationRemoved(application->appId());
189196 Q_EMIT countChanged();
190197 if (isEmpty()) Q_EMIT emptyChanged(isEmpty());
191198 }
@@ -209,18 +216,29 @@ void ApplicationManager::move(int from, int to) {
209216ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
210217 const QStringList &arguments)
211218{
219+ DEBUG_MSG(appId);
212220 Q_UNUSED(arguments)
213− ApplicationInfo *application = add(appId);
221+
222+ ApplicationInfo *application = findApplication(appId);
223+ if (application) {
224+ // the requested app is already running
225+ return application;
226+ } else {
227+ application = add(appId);
228+ }
229+
230+ // most likely not among the available ones
214231 if (!application)
215− return 0;
216− application->setState(ApplicationInfo::Starting);
232+ return nullptr;
233+
234+ Q_EMIT application->focusRequested(); // we assume that an application that's starting up wants focus
217235
218236 return application;
219237}
220238
221239ApplicationInfo* ApplicationManager::add(QString appId)
222240{
223− ApplicationInfo *application = 0;
241+ ApplicationInfo *application = nullptr;
224242
225243 for (ApplicationInfo *availableApp : m_availableApplications) {
226244 if (availableApp->appId() == appId) {
@@ -229,23 +247,23 @@ ApplicationInfo* ApplicationManager::add(QString appId)
229247 }
230248 }
231249
232− if (application)
233− add(application);
250+ if (application) {
251+ if (!add(application)) {
252+ application = nullptr;
253+ }
254+ }
234255
235256 return application;
236257}
237258
238259bool ApplicationManager::stopApplication(const QString &appId)
239260{
261+ DEBUG_MSG(appId);
240262 ApplicationInfo *application = findApplication(appId);
241263 if (application == nullptr)
242264 return false;
243265
244− if (application->appId() == focusedApplicationId()) {
245− unfocusCurrentApplication();
246− }
247− application->setState(ApplicationInfo::Stopped);
248− remove(application);
266+ application->close();
249267 return true;
250268}
251269
@@ -258,44 +276,17 @@ QString ApplicationManager::focusedApplicationId() const {
258276 return QString();
259277}
260278
261−bool ApplicationManager::focusApplication(const QString &appId)
279+bool ApplicationManager::requestFocusApplication(const QString &appId)
262280{
263281 ApplicationInfo *application = findApplication(appId);
264282 if (application == nullptr)
265283 return false;
266284
267− // unfocus currently focused app
268− for (ApplicationInfo *app : m_runningApplications) {
269− if (app->focused()) {
270− app->setFocused(false);
271− }
272− }
273−
274− // focus this app
275− application->setFocused(true);
285+ application->requestFocus();
276286
277− // move app to top of stack
278− move(m_runningApplications.indexOf(application), 0);
279− Q_EMIT focusedApplicationIdChanged();
280287 return true;
281288}
282289
283−bool ApplicationManager::requestFocusApplication(const QString &appId)
284−{
285− QMetaObject::invokeMethod(this, "focusRequested", Qt::QueuedConnection, Q_ARG(QString, appId));
286− return true;
287−}
288−
289−void ApplicationManager::unfocusCurrentApplication()
290−{
291− for (ApplicationInfo *app : m_runningApplications) {
292− if (app->focused()) {
293− app->setFocused(false);
294− }
295− }
296− Q_EMIT focusedApplicationIdChanged();
297−}
298−
299290void ApplicationManager::buildListOfAvailableApplications()
300291{
301292 /*
@@ -328,7 +319,6 @@ void ApplicationManager::buildListOfAvailableApplications()
328319 application->setName("Camera");
329320 application->setScreenshotId("camera");
330321 application->setIconId("camera");
331− application->setShellChrome(Mir::LowChrome);
332322 application->setFullscreen(true);
333323 application->setSupportedOrientations(Qt::PortraitOrientation
334324 | Qt::LandscapeOrientation
@@ -473,3 +463,48 @@ bool ApplicationManager::isEmpty() const
473463{
474464 return m_runningApplications.isEmpty();
475465}
466+
467+void ApplicationManager::updateFocusedApplication()
468+{
469+ ApplicationInfo *focusedApplication = nullptr;
470+ ApplicationInfo *previouslyFocusedApplication = nullptr;
471+
472+ auto controller = MirFocusController::instance();
473+ if (!controller) {
474+ return;
475+ }
476+
477+ MirSurface *surface = static_cast<MirSurface*>(controller->focusedSurface());
478+ if (surface) {
479+ focusedApplication = findApplication(surface);
480+ }
481+
482+ surface = static_cast<MirSurface*>(controller->previouslyFocusedSurface());
483+ if (surface) {
484+ previouslyFocusedApplication = findApplication(surface);
485+ }
486+
487+ if (focusedApplication != previouslyFocusedApplication) {
488+ if (focusedApplication) {
489+ DEBUG_MSG("focused " << focusedApplication->appId());
490+ Q_EMIT focusedApplication->focusedChanged(true);
491+ this->move(this->m_runningApplications.indexOf(focusedApplication), 0);
492+ }
493+ if (previouslyFocusedApplication) {
494+ DEBUG_MSG("unfocused " << previouslyFocusedApplication->appId());
495+ Q_EMIT previouslyFocusedApplication->focusedChanged(false);
496+ }
497+ Q_EMIT focusedApplicationIdChanged();
498+ }
499+}
500+
501+ApplicationInfo *ApplicationManager::findApplication(MirSurface* surface)
502+{
503+ for (ApplicationInfo *app : m_runningApplications) {
504+ auto surfaceList = static_cast<MirSurfaceListModel*>(app->surfaceList());
505+ if (surfaceList->contains(surface)) {
506+ return app;
507+ }
508+ }
509+ return nullptr;
510+}
tests/mocks/Unity/Application/ApplicationManager.h+12−14
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013-2014 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -26,12 +26,19 @@
2626// unity-api
2727#include <unity/shell/application/ApplicationManagerInterface.h>
2828
29+namespace unity {
30+ namespace shell {
31+ namespace application {
32+ class MirSurfaceInterface;
33+ }
34+ }
35+}
36+
2937class QQuickItem;
3038using namespace unity::shell::application;
3139
3240class ApplicationManager : public ApplicationManagerInterface {
3341 Q_OBJECT
34− Q_FLAGS(ExecFlags)
3542
3643 Q_PROPERTY(bool empty READ isEmpty NOTIFY emptyChanged)
3744 Q_PROPERTY(QStringList availableApplications READ availableApplications NOTIFY availableApplicationsChanged)
@@ -40,13 +47,6 @@ class ApplicationManager : public ApplicationManagerInterface {
4047 ApplicationManager(QObject *parent = nullptr);
4148 virtual ~ApplicationManager();
4249
43− static ApplicationManager *singleton();
44−
45− enum MoreRoles {
46− RoleSession = RoleExemptFromLifecycle+1,
47− RoleFullscreen,
48− };
49−
5050 // QAbstractItemModel methods.
5151 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
5252 QVariant data(const QModelIndex& index, int role) const override;
@@ -57,8 +57,6 @@ class ApplicationManager : public ApplicationManagerInterface {
5757
5858 // Application control methods
5959 Q_INVOKABLE bool requestFocusApplication(const QString &appId) override;
60− Q_INVOKABLE bool focusApplication(const QString &appId) override;
61− Q_INVOKABLE void unfocusCurrentApplication() override;
6260 Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, const QStringList &arguments = QStringList()) override;
6361 Q_INVOKABLE bool stopApplication(const QString &appId) override;
6462
@@ -79,17 +77,17 @@ class ApplicationManager : public ApplicationManagerInterface {
7977
8078 private Q_SLOTS:
8179 void onWindowCreatedTimerTimeout();
80+ void updateFocusedApplication();
8281
8382 private:
84− void add(ApplicationInfo *application);
83+ bool add(ApplicationInfo *application);
8584 void remove(ApplicationInfo* application);
8685 void buildListOfAvailableApplications();
8786 void onWindowCreated();
87+ ApplicationInfo *findApplication(MirSurface* surface);
8888 QList<ApplicationInfo*> m_runningApplications;
8989 QList<ApplicationInfo*> m_availableApplications;
9090 QTimer m_windowCreatedTimer;
91−
92− static ApplicationManager *the_application_manager;
9391};
9492
9593Q_DECLARE_METATYPE(ApplicationManager*)
tests/mocks/Unity/Application/ApplicationTestInterface.cppdeleted−117
@@ -1,117 +0,0 @@
1−/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "ApplicationInfo.h"
18−#include "ApplicationTestInterface.h"
19−#include "ApplicationManager.h"
20−#include "SessionManager.h"
21−#include "SurfaceManager.h"
22−#include "Session.h"
23−#include "MirSurface.h"
24−
25−#include <paths.h>
26−
27−#include <QtDBus/QtDBus>
28−
29−quint32 nextId = 0;
30−
31−ApplicationTestInterface::ApplicationTestInterface(QObject* parent)
32− : QDBusAbstractAdaptor(parent)
33−{
34− QDBusConnection connection = QDBusConnection::sessionBus();
35− connection.registerService("com.canonical.Unity8");
36− connection.registerObject("/com/canonical/Unity8/Mocks", parent);
37−}
38−
39−Session* ApplicationTestInterface::addChildSession(Session* existingSession, const QString& surfaceImage, bool addSurface)
40−{
41− if (!existingSession) return nullptr;
42−
43− QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%2@12.png")
44− .arg(qmlDirectory())
45− .arg(surfaceImage);
46−
47− Session* session = SessionManager::singleton()->createSession(
48− QString("%1-Child%2").arg(existingSession->name())
49− .arg(existingSession->childSessions()->count()),
50− screenshotUrl);
51− existingSession->addChildSession(session);
52− if (addSurface) session->createSurface();
53−
54− return session;
55−}
56−
57−void ApplicationTestInterface::removeSession(Session* existingSession)
58−{
59− qDebug() << "ApplicationTestInterface::removeSession - " << existingSession;
60−
61− if (existingSession)
62− existingSession->setLive(false);
63−}
64−
65−void ApplicationTestInterface::removeSurface(MirSurface* existingSurface)
66−{
67− qDebug() << "ApplicationTestInterface::removeSurface - " << existingSurface;
68−
69− if (existingSurface)
70− existingSurface->setLive(false);
71−}
72−
73−quint32 ApplicationTestInterface::addChildSession(const QString& appId, quint32 existingSessionId, const QString& surfaceImage, bool addSurface)
74−{
75− qDebug() << "ApplicationTestInterface::addChildSession to " << appId;
76−
77− ApplicationInfo* application = ApplicationManager::singleton()->findApplication(appId);
78− if (!application) {
79− qDebug() << "ApplicationTestInterface::addChildSession - No application found for " << appId;
80− return 0;
81− }
82−
83− Session* parentSession = nullptr;
84− if (m_childSessions.contains(existingSessionId)) {
85− parentSession = m_childSessions[existingSessionId];
86− } else if (application->session()) {
87− parentSession = application->session();
88− } else {
89− qDebug() << "ApplicationTestInterface::addChildSession - No session for " << appId << ":" << existingSessionId;
90− return 0;
91− }
92−
93− QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%2@12.png")
94− .arg(qmlDirectory())
95− .arg(surfaceImage);
96−
97− quint32 sessionId = ++nextId;
98− Session* session = SessionManager::singleton()->createSession(
99− QString("%1-Child%2").arg(parentSession->name())
100− .arg(parentSession->childSessions()->count()),
101− screenshotUrl);
102− parentSession->addChildSession(session);
103− if (addSurface) { session->createSurface(); }
104− m_childSessions[sessionId] = session;
105−
106− return sessionId;
107−}
108−
109−void ApplicationTestInterface::removeSession(quint32 sessionId)
110−{
111− if (!m_childSessions.contains(sessionId)) {
112− qDebug() << "ApplicationTestInterface::removeSession - No added session for " << sessionId;
113− return;
114− }
115− Session* session = m_childSessions.take(sessionId);
116− removeSession(session);
117−}
tests/mocks/Unity/Application/ApplicationTestInterface.hdeleted−45
@@ -1,45 +0,0 @@
1−/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef APPLICATIONTESTINTERFACE_H
18−#define APPLICATIONTESTINTERFACE_H
19−
20−#include <QtDBus/QtDBus>
21−
22−class ApplicationManager;
23−class Session;
24−class MirSurface;
25−
26−class ApplicationTestInterface : public QDBusAbstractAdaptor
27−{
28− Q_OBJECT
29− Q_CLASSINFO("D-Bus Interface", "com.canonical.Unity8.Mocks.Application")
30−public:
31− ApplicationTestInterface(QObject* parent = 0);
32−
33− Q_INVOKABLE Session* addChildSession(Session* existingSession, const QString& surfaceImage, bool addSurface);
34− Q_INVOKABLE void removeSession(Session* existingSession);
35− Q_INVOKABLE void removeSurface(MirSurface* existingSurface);
36−
37−public Q_SLOTS:
38− quint32 addChildSession(const QString& appId, quint32 existingSessionId, const QString& surfaceImage, bool addSurface);
39− void removeSession(quint32 sessionId);
40−
41−private:
42− QHash<quint32, Session*> m_childSessions;
43−};
44−
45−#endif // APPLICATIONTESTINTERFACE_H
tests/mocks/Unity/Application/CMakeLists.txt+3−4
@@ -2,14 +2,11 @@ set(FakeUnityApplicationQml_SOURCES
22 plugin.cpp
33 ApplicationInfo.cpp
44 ApplicationManager.cpp
5− ApplicationTestInterface.cpp
65 MirSurface.cpp
76 MirSurfaceItem.cpp
7+ MirSurfaceListModel.cpp
88 ObjectListModel.h
9− Session.cpp
10− SessionManager.cpp
119 SurfaceManager.cpp
12− SessionModel.h
1310 UbuntuKeyboardInfo.cpp
1411 VirtualKeyboard.cpp
1512 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
@@ -17,6 +14,8 @@ set(FakeUnityApplicationQml_SOURCES
1714 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
1815 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
1916 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceItemInterface.h
17+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceListInterface.h
18+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirFocusControllerInterface.h
2019 resources/surfaces.qrc
2120)
2221
tests/mocks/Unity/Application/MirSurface.cpp+175−10
@@ -19,13 +19,49 @@
1919#include <QDebug>
2020#include <QQmlEngine>
2121
22+#define MIRSURFACE_DEBUG 0
23+
24+#if MIRSURFACE_DEBUG
25+#define DEBUG_MSG(params) qDebug().nospace() << "MirSurface[" << (void*)this << "," << m_name << "]::" << __func__ << " " << params
26+
27+const char *stateToStr(Mir::State state)
28+{
29+ switch(state) {
30+ case Mir::UnknownState:
31+ return "unknown";
32+ case Mir::RestoredState:
33+ return "restored";
34+ case Mir::MinimizedState:
35+ return "minimized";
36+ case Mir::MaximizedState:
37+ return "maximized";
38+ case Mir::VertMaximizedState:
39+ return "vert-maximized";
40+ case Mir::FullscreenState:
41+ return "fullscreen";
42+ case Mir::HorizMaximizedState:
43+ return "horiz-maximized";
44+ case Mir::HiddenState:
45+ return "hidden";
46+ default:
47+ return "???";
48+ };
49+}
50+
51+#else
52+#define DEBUG_MSG(params) ((void)0)
53+#endif
54+
55+using namespace unity::shell::application;
56+
57+MirFocusController *MirFocusController::m_instance = nullptr;
58+
2259MirSurface::MirSurface(const QString& name,
2360 Mir::Type type,
2461 Mir::State state,
2562 const QUrl& screenshot,
26− const QUrl &qmlFilePath,
27− QObject *parent)
28− : unity::shell::application::MirSurfaceInterface(parent)
63+ const QUrl &qmlFilePath)
64+ : unity::shell::application::MirSurfaceInterface(nullptr)
2965 , m_name(name)
3066 , m_type(type)
3167 , m_state(state)
@@ -40,17 +76,28 @@ MirSurface::MirSurface(const QString& name,
4076 , m_slowToResize(false)
4177 , m_shellChrome(Mir::NormalChrome)
4278{
43−// qDebug() << "MirSurface::MirSurface() " << name;
79+ DEBUG_MSG("");
80+
4481 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
4582
4683 m_delayedResizeTimer.setInterval(600);
4784 m_delayedResizeTimer.setSingleShot(true);
4885 connect(&m_delayedResizeTimer, &QTimer::timeout, this, &MirSurface::applyDelayedResize);
86+
87+ m_zombieTimer.setInterval(100);
88+ m_zombieTimer.setSingleShot(true);
89+ connect(&m_zombieTimer, &QTimer::timeout, this, [this](){ this->setLive(false); });
4990}
5091
5192MirSurface::~MirSurface()
5293{
53−// qDebug() << "MirSurface::~MirSurface() " << name();
94+ DEBUG_MSG("");
95+
96+ // controller instance might have been already destroyed by QQmlEngine destructor
97+ auto controller = MirFocusController::instance();
98+ if (controller && controller->focusedSurface() == this) {
99+ controller->clear();
100+ }
54101}
55102
56103QString MirSurface::name() const
@@ -73,6 +120,7 @@ void MirSurface::setState(Mir::State state)
73120 if (state == m_state)
74121 return;
75122
123+ DEBUG_MSG(stateToStr(state));
76124 m_state = state;
77125 Q_EMIT stateChanged(state);
78126}
@@ -89,10 +137,10 @@ bool MirSurface::visible() const
89137
90138void MirSurface::setLive(bool live)
91139{
92−// qDebug().nospace() << "MirSurface::setLive("<<live<<") " << name();
93140 if (live == m_live)
94141 return;
95142
143+ DEBUG_MSG(live);
96144 m_live = live;
97145 Q_EMIT liveChanged(live);
98146
@@ -137,6 +185,7 @@ void MirSurface::setOrientationAngle(Mir::OrientationAngle angle)
137185void MirSurface::setKeymap(const QString &value)
138186{
139187 if (value != m_keymap) {
188+ DEBUG_MSG(value);
140189 m_keymap = value;
141190 Q_EMIT keymapChanged(m_keymap);
142191 }
@@ -157,6 +206,7 @@ void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
157206 if (shellChrome == m_shellChrome)
158207 return;
159208
209+ DEBUG_MSG(shellChrome);
160210 m_shellChrome = shellChrome;
161211 Q_EMIT shellChromeChanged(shellChrome);
162212}
@@ -164,15 +214,13 @@ void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
164214void MirSurface::registerView(qintptr viewId)
165215{
166216 m_views.insert(viewId, MirSurface::View{false});
167−// qDebug().nospace() << "MirSurface[" << name() << "]::registerView(" << viewId << ")"
168−// << " after=" << m_views.count();
217+ DEBUG_MSG(viewId << " after=" << m_views.count());
169218}
170219
171220void MirSurface::unregisterView(qintptr viewId)
172221{
173−// qDebug().nospace() << "MirSurface[" << name() << "]::unregisterView(" << viewId << ")"
174−// << " after=" << m_views.count() << " live=" << m_live;
175222 m_views.remove(viewId);
223+ DEBUG_MSG(viewId << " after=" << m_views.count() << " live=" << m_live);
176224 if (!m_live && m_views.count() == 0) {
177225 deleteLater();
178226 }
@@ -288,6 +336,7 @@ bool MirSurface::isSlowToResize() const
288336void MirSurface::setSlowToResize(bool value)
289337{
290338 if (m_slowToResize != value) {
339+ DEBUG_MSG(value);
291340 m_slowToResize = value;
292341 Q_EMIT slowToResizeChanged();
293342 if (!m_slowToResize && m_delayedResizeTimer.isActive()) {
@@ -344,3 +393,119 @@ void MirSurface::setHeightIncrement(int value)
344393 Q_EMIT heightIncrementChanged(m_heightIncrement);
345394 }
346395}
396+
397+void MirSurface::raise()
398+{
399+ Q_EMIT raiseRequested();
400+}
401+
402+void MirSurface::close()
403+{
404+ DEBUG_MSG("");
405+ m_zombieTimer.start();
406+ Q_EMIT closeRequested();
407+}
408+
409+void MirSurface::createPromptSurface()
410+{
411+ QStringList screenshotIds = {"gallery", "map", "facebook", "camera", "browser", "music", "twitter"};
412+ int i = rand() % screenshotIds.count();
413+
414+ QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%1@12.png")
415+ .arg(screenshotIds[i]);
416+
417+ auto surface = new MirSurface(QString("prompt foo"),
418+ Mir::NormalType,
419+ Mir::RestoredState,
420+ screenshotUrl);
421+
422+ m_promptSurfaceList.appendSurface(surface);
423+}
424+
425+MirSurfaceListInterface* MirSurface::promptSurfaceList()
426+{
427+ return &m_promptSurfaceList;
428+}
429+
430+void MirSurface::requestFocus()
431+{
432+ DEBUG_MSG("");
433+ Q_EMIT focusRequested();
434+}
435+
436+void MirSurface::setFocused(bool value)
437+{
438+ DEBUG_MSG(value);
439+
440+ auto controller = MirFocusController::instance();
441+ // controller instance might have been already destroyed by QQmlEngine destructor
442+ if (!controller) {
443+ return;
444+ }
445+
446+ if (value) {
447+ controller->setFocusedSurface(this);
448+ } else if (controller->focusedSurface() == this) {
449+ controller->setFocusedSurface(nullptr);
450+ }
451+}
452+
453+bool MirSurface::focused() const
454+{
455+ auto controller = MirFocusController::instance();
456+
457+ // controller instance might have been already destroyed by QQmlEngine destructor
458+ return controller ? controller->focusedSurface() == this : false;
459+}
460+
461+#if MIRSURFACE_DEBUG
462+#undef DEBUG_MSG
463+#define DEBUG_MSG(params) qDebug().nospace() << "MirFocusController::" << __func__ << " " << params
464+#endif
465+
466+void MirFocusController::setFocusedSurface(MirSurfaceInterface *surface)
467+{
468+ if (m_focusedSurface == surface) {
469+ return;
470+ }
471+ DEBUG_MSG("MirSurface[" << (void*)surface << "," << (surface?surface->name():"") << "]");
472+
473+ m_previouslyFocusedSurface = m_focusedSurface;
474+ m_focusedSurface = surface;
475+
476+ if (m_previouslyFocusedSurface != m_focusedSurface) {
477+ Q_EMIT focusedSurfaceChanged();
478+ }
479+
480+ if (m_previouslyFocusedSurface) {
481+ Q_EMIT m_previouslyFocusedSurface->focusedChanged(false);
482+ }
483+
484+ if (m_focusedSurface) {
485+ Q_EMIT m_focusedSurface->focusedChanged(true);
486+ }
487+}
488+
489+MirFocusController* MirFocusController::instance()
490+{
491+ return m_instance;
492+}
493+
494+MirFocusController::MirFocusController()
495+{
496+ DEBUG_MSG("");
497+ Q_ASSERT(m_instance == nullptr);
498+ m_instance = this;
499+}
500+
501+MirFocusController::~MirFocusController()
502+{
503+ Q_ASSERT(m_instance == this);
504+ m_instance = nullptr;
505+}
506+
507+void MirFocusController::clear()
508+{
509+ m_focusedSurface = m_previouslyFocusedSurface = nullptr;
510+ Q_EMIT focusedSurfaceChanged();
511+}
tests/mocks/Unity/Application/MirSurface.h+51−7
@@ -23,8 +23,31 @@
2323#include <QHash>
2424
2525// unity-api
26+#include <unity/shell/application/MirFocusControllerInterface.h>
2627#include <unity/shell/application/MirSurfaceInterface.h>
2728
29+#include "MirSurfaceListModel.h"
30+
31+class MirSurface;
32+
33+class MirFocusController : public unity::shell::application::MirFocusControllerInterface
34+{
35+ Q_OBJECT
36+public:
37+ MirFocusController();
38+ virtual ~MirFocusController();
39+ static MirFocusController* instance();
40+
41+ void setFocusedSurface(unity::shell::application::MirSurfaceInterface *surface) override;
42+ unity::shell::application::MirSurfaceInterface* focusedSurface() const override { return m_focusedSurface; }
43+ unity::shell::application::MirSurfaceInterface* previouslyFocusedSurface() { return m_previouslyFocusedSurface; }
44+ void clear();
45+private:
46+ static MirFocusController *m_instance;
47+ unity::shell::application::MirSurfaceInterface* m_previouslyFocusedSurface{nullptr};
48+ unity::shell::application::MirSurfaceInterface* m_focusedSurface{nullptr};
49+};
50+
2851class MirSurface : public unity::shell::application::MirSurfaceInterface
2952{
3053 Q_OBJECT
@@ -41,8 +64,7 @@ public:
4164 Mir::Type type,
4265 Mir::State state,
4366 const QUrl& screenshot,
44− const QUrl &qmlFilePath = QUrl(),
45− QObject *parent = nullptr);
67+ const QUrl &qmlFilePath = QUrl());
4668 virtual ~MirSurface();
4769
4870 ////
@@ -79,17 +101,22 @@ public:
79101
80102 Mir::ShellChrome shellChrome() const override;
81103
104+ bool focused() const override;
105+
106+ Q_INVOKABLE void requestFocus() override;
107+
108+ Q_INVOKABLE void close() override;
109+
110+ unity::shell::application::MirSurfaceListInterface* promptSurfaceList() override;
111+
112+ Q_INVOKABLE void raise() override;
113+
82114 ////
83115 // API for tests
84116
85117 Q_INVOKABLE void setLive(bool live);
86118 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
87119
88− void registerView(qintptr viewId);
89− void unregisterView(qintptr viewId);
90− void setViewVisibility(qintptr viewId, bool visible);
91− int viewCount() const { return m_views.count(); }
92−
93120 int width() const;
94121 int height() const;
95122
@@ -103,6 +130,8 @@ public:
103130 Q_INVOKABLE void setWidthIncrement(int);
104131 Q_INVOKABLE void setHeightIncrement(int);
105132
133+ Q_INVOKABLE void createPromptSurface();
134+
106135 /////
107136 // internal mock stuff
108137
@@ -114,7 +143,16 @@ public:
114143 bool activeFocus() const;
115144 void setActiveFocus(bool);
116145
146+ void registerView(qintptr viewId);
147+ void unregisterView(qintptr viewId);
148+ void setViewVisibility(qintptr viewId, bool visible);
149+ int viewCount() const { return m_views.count(); }
150+
151+ void setFocused(bool value);
152+
117153Q_SIGNALS:
154+ ////
155+ // API for tests
118156 void widthChanged();
119157 void heightChanged();
120158 void slowToResizeChanged();
@@ -123,6 +161,8 @@ Q_SIGNALS:
123161 // internal mock stuff
124162 void screenshotUrlChanged(QUrl);
125163 void activeFocusChanged(bool);
164+ void raiseRequested();
165+ void closeRequested();
126166
127167private Q_SLOTS:
128168 void applyDelayedResize();
@@ -135,6 +175,7 @@ private:
135175 const Mir::Type m_type;
136176 Mir::State m_state;
137177 Mir::OrientationAngle m_orientationAngle;
178+
138179 QUrl m_screenshotUrl;
139180 QUrl m_qmlFilePath;
140181 bool m_live;
@@ -158,11 +199,14 @@ private:
158199 QSize m_pendingResize;
159200
160201 Mir::ShellChrome m_shellChrome;
202+ MirSurfaceListModel m_promptSurfaceList;
161203
162204 struct View {
163205 bool visible;
164206 };
165207 QHash<qintptr, View> m_views;
208+
209+ QTimer m_zombieTimer;
166210};
167211
168212#endif // MOCK_MIR_SURFACE_H
tests/mocks/Unity/Application/MirSurfaceItem.cpp+13−7
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -29,6 +29,14 @@
2929
3030using namespace unity::shell::application;
3131
32+#define MIRSURFACEITEM_DEBUG 0
33+
34+#if MIRSURFACEITEM_DEBUG
35+#define DEBUG_MSG(params) qDebug().nospace() << "MirSurfaceItem::" << __func__ << " " << params
36+#else
37+#define DEBUG_MSG(params) ((void)0)
38+#endif
39+
3240MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
3341 : MirSurfaceItemInterface(parent)
3442 , m_qmlSurface(nullptr)
@@ -41,7 +49,7 @@ MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
4149 , m_mousePressCount(0)
4250 , m_mouseReleaseCount(0)
4351{
44−// qDebug() << "MirSurfaceItem::MirSurfaceItem() " << (void*)(this) << name();
52+ DEBUG_MSG((void*)(this) << name());
4553 setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton |
4654 Qt::ExtraButton1 | Qt::ExtraButton2 | Qt::ExtraButton3 | Qt::ExtraButton4 |
4755 Qt::ExtraButton5 | Qt::ExtraButton6 | Qt::ExtraButton7 | Qt::ExtraButton8 |
@@ -53,7 +61,7 @@ MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
5361
5462MirSurfaceItem::~MirSurfaceItem()
5563{
56−// qDebug() << "MirSurfaceItem::~MirSurfaceItem() " << (void*)(this) << name();
64+ DEBUG_MSG((void*)(this) << name());
5765 setSurface(nullptr);
5866}
5967
@@ -150,7 +158,7 @@ void MirSurfaceItem::onComponentStatusChanged(QQmlComponent::Status status)
150158
151159void MirSurfaceItem::createQmlContentItem()
152160{
153−// qDebug() << "MirSurfaceItem::createQmlContentItem()";
161+ DEBUG_MSG("");
154162
155163 m_qmlItem = qobject_cast<QQuickItem*>(m_qmlContentComponent->create());
156164 m_qmlItem->setParentItem(this);
@@ -213,9 +221,7 @@ void MirSurfaceItem::mouseReleaseEvent(QMouseEvent * event)
213221
214222void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
215223{
216−// qDebug().nospace() << "MirSurfaceItem::setSurface() this=" << (void*)(this)
217−// << " name=" << name()
218−// << " surface=" << surface;
224+ DEBUG_MSG("this=" << (void*)(this) << " name=" << name() << " surface=" << surface);
219225
220226 if (m_qmlSurface == surface) {
221227 return;
tests/mocks/Unity/Application/MirSurfaceListModel.cppnew file+121
@@ -0,0 +1,121 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "MirSurfaceListModel.h"
18+#include "ApplicationInfo.h"
19+
20+#include "MirSurface.h"
21+
22+#define MIRSURFACELISTMODEL_DEBUG 0
23+
24+#ifdef MIRSURFACELISTMODEL_DEBUG
25+#define DEBUG_MSG(params) qDebug().nospace() << "MirSurfaceListModel::" << __func__ << " " << params
26+#else
27+#define DEBUG_MSG(params) ((void)0)
28+#endif
29+
30+using namespace unity::shell::application;
31+
32+MirSurfaceListModel::MirSurfaceListModel(QObject *parent) :
33+ MirSurfaceListInterface(parent)
34+{
35+}
36+
37+int MirSurfaceListModel::rowCount(const QModelIndex &parent) const
38+{
39+ return !parent.isValid() ? m_surfaceList.size() : 0;
40+}
41+
42+QVariant MirSurfaceListModel::data(const QModelIndex& index, int role) const
43+{
44+ if (index.row() < 0 || index.row() >= m_surfaceList.size())
45+ return QVariant();
46+
47+ if (role == SurfaceRole) {
48+ MirSurface *surface = m_surfaceList.at(index.row());
49+ return QVariant::fromValue(static_cast<unity::shell::application::MirSurfaceInterface*>(surface));
50+ } else {
51+ return QVariant();
52+ }
53+}
54+
55+void MirSurfaceListModel::raise(MirSurface *surface)
56+{
57+ int i = m_surfaceList.indexOf(surface);
58+ if (i != -1) {
59+ moveSurface(i, 0);
60+ }
61+}
62+
63+void MirSurfaceListModel::appendSurface(MirSurface *surface)
64+{
65+ beginInsertRows(QModelIndex(), m_surfaceList.size(), m_surfaceList.size());
66+ m_surfaceList.append(surface);
67+ connectSurface(surface);
68+ endInsertRows();
69+ Q_EMIT countChanged();
70+}
71+
72+void MirSurfaceListModel::connectSurface(MirSurface *surface)
73+{
74+ connect(surface, &MirSurface::raiseRequested, this, [this, surface](){ this->raise(surface); });
75+ connect(surface, &QObject::destroyed, this, [this, surface](){ this->removeSurface(surface); });
76+}
77+
78+void MirSurfaceListModel::removeSurface(MirSurface *surface)
79+{
80+ int i = m_surfaceList.indexOf(surface);
81+ if (i != -1) {
82+ beginRemoveRows(QModelIndex(), i, i);
83+ m_surfaceList.removeAt(i);
84+ endRemoveRows();
85+ Q_EMIT countChanged();
86+ }
87+}
88+
89+void MirSurfaceListModel::moveSurface(int from, int to)
90+{
91+ if (from == to) return;
92+
93+ if (from >= 0 && from < m_surfaceList.size() && to >= 0 && to < m_surfaceList.size()) {
94+ QModelIndex parent;
95+ /* When moving an item down, the destination index needs to be incremented
96+ by one, as explained in the documentation:
97+ http://qt-project.org/doc/qt-5.0/qtcore/qabstractitemmodel.html#beginMoveRows */
98+
99+ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
100+ m_surfaceList.move(from, to);
101+ endMoveRows();
102+ }
103+}
104+
105+MirSurfaceInterface *MirSurfaceListModel::get(int index)
106+{
107+ if (index >=0 && index < m_surfaceList.count()) {
108+ return m_surfaceList[index];
109+ } else {
110+ return nullptr;
111+ }
112+}
113+
114+const MirSurfaceInterface *MirSurfaceListModel::get(int index) const
115+{
116+ if (index >=0 && index < m_surfaceList.count()) {
117+ return m_surfaceList.at(index);
118+ } else {
119+ return nullptr;
120+ }
121+}
tests/mocks/Unity/Application/MirSurfaceListModel.hnew file+56
@@ -0,0 +1,56 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef MIRSURFACELISTMODEL_H
18+#define MIRSURFACELISTMODEL_H
19+
20+// unity-api
21+#include <unity/shell/application/MirSurfaceListInterface.h>
22+
23+#include <QAbstractListModel>
24+#include <QList>
25+
26+class MirSurface;
27+
28+class MirSurfaceListModel : public unity::shell::application::MirSurfaceListInterface
29+{
30+ Q_OBJECT
31+public:
32+ explicit MirSurfaceListModel(QObject *parent = 0);
33+
34+ Q_INVOKABLE unity::shell::application::MirSurfaceInterface *get(int index) override;
35+ const unity::shell::application::MirSurfaceInterface *get(int index) const;
36+
37+ // QAbstractItemModel methods
38+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
39+ QVariant data(const QModelIndex& index, int role) const override;
40+
41+ void appendSurface(MirSurface *surface);
42+ void removeSurface(MirSurface *surface);
43+
44+ bool contains(MirSurface *surface) const { return m_surfaceList.contains(surface); }
45+
46+private:
47+ void raise(MirSurface *surface);
48+ void moveSurface(int from, int to);
49+ void connectSurface(MirSurface *surface);
50+
51+ QList<MirSurface*> m_surfaceList;
52+};
53+
54+Q_DECLARE_METATYPE(MirSurfaceListModel*)
55+
56+#endif // MIRSURFACELISTMODEL_H
tests/mocks/Unity/Application/ObjectListModel.h+2
@@ -20,6 +20,8 @@
2020// Qt
2121#include <QAbstractListModel>
2222
23+#include <QDebug>
24+
2325template<class TYPE>
2426class ObjectListModel : public QAbstractListModel
2527{
tests/mocks/Unity/Application/Session.cppdeleted−209
@@ -1,209 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "Session.h"
18−#include "ApplicationInfo.h"
19−#include "SessionManager.h"
20−#include "SurfaceManager.h"
21−
22−#include <QPainter>
23−#include <QQmlEngine>
24−#include <QTimer>
25−
26−Session::Session(const QString &name,
27− const QUrl& screenshot,
28− QObject *parent)
29− : QObject(parent)
30− , m_name(name)
31− , m_live(true)
32− , m_screenshot(screenshot)
33− , m_application(nullptr)
34− , m_surface(nullptr)
35− , m_parentSession(nullptr)
36− , m_children(new SessionModel(this))
37− , m_fullscreen(false)
38−{
39−// qDebug() << "Session::Session() " << this->name();
40−
41− QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
42−}
43−
44−Session::~Session()
45−{
46−// qDebug() << "Session::~Session() " << name();
47−
48− QList<Session*> children(m_children->list());
49− for (Session* child : children) {
50− delete child;
51− }
52− if (m_parentSession) {
53− m_parentSession->removeChildSession(this);
54− }
55− if (m_application) {
56− m_application->setSession(nullptr);
57− }
58− delete m_children;
59−
60− if (m_surface) {
61− if (m_surface->viewCount() == 0) {
62− delete m_surface;
63− } else {
64− m_surface->setLive(false);
65− }
66− }
67−}
68−
69−void Session::release()
70−{
71−// qDebug() << "Session::release " << name();
72− deleteLater();
73−}
74−
75−void Session::updateFullscreenProperty()
76−{
77− if (m_surfaces.rowCount() > 0) {
78− // TODO: Figure out something better
79− setFullscreen(lastSurface()->state() == Mir::FullscreenState);
80− } else {
81− // Keep the current value of the fullscreen property until we get a new
82− // surface
83− }
84−}
85−
86−void Session::setFullscreen(bool fullscreen)
87−{
88− if (m_fullscreen != fullscreen) {
89− m_fullscreen = fullscreen;
90− Q_EMIT fullscreenChanged(m_fullscreen);
91− }
92−}
93−
94−void Session::setApplication(ApplicationInfo* application)
95−{
96− if (m_application == application)
97− return;
98−
99− m_application = application;
100− Q_EMIT applicationChanged(application);
101−}
102−
103−void Session::appendSurface(MirSurface* surface)
104−{
105− qDebug() << "Session::appendSurface - session=" << name() << "surface=" << surface;
106−
107− m_surfaces.insert(m_surfaces.rowCount(), surface);
108−
109− connect(surface, &MirSurfaceInterface::stateChanged, this, &Session::updateFullscreenProperty);
110− connect(surface, &QObject::destroyed,
111− this, [this, surface]() { this->removeSurface(surface); });
112−
113− Q_EMIT lastSurfaceChanged(surface);
114− Q_EMIT surfaceAdded(surface);
115−
116− updateFullscreenProperty();
117−}
118−
119−void Session::removeSurface(MirSurface* surface)
120−{
121− disconnect(surface, nullptr, this, nullptr);
122− if (m_surfaces.contains(surface)) {
123− m_surfaces.remove(surface);
124− }
125−
126− updateFullscreenProperty();
127−}
128−
129−void Session::setScreenshot(const QUrl& screenshot)
130−{
131− if (screenshot != m_screenshot) {
132− m_screenshot = screenshot;
133− if (m_surface) {
134− m_surface->setScreenshotUrl(m_screenshot);
135− }
136− }
137−}
138−
139−void Session::setLive(bool live)
140−{
141− if (m_live != live) {
142− m_live = live;
143− Q_EMIT liveChanged(m_live);
144− }
145−}
146−
147−void Session::setParentSession(Session* session)
148−{
149− if (m_parentSession == session || session == this)
150− return;
151−
152− m_parentSession = session;
153− Q_EMIT parentSessionChanged(session);
154−}
155−
156−void Session::createSurface()
157−{
158− if (m_surface) return;
159−
160− appendSurface(SurfaceManager::singleton()->createSurface(name(),
161− Mir::NormalType,
162− m_application && m_application->fullscreen() ? Mir::FullscreenState :
163− Mir::MaximizedState,
164− m_screenshot));
165−}
166−
167−void Session::addChildSession(Session* session)
168−{
169− insertChildSession(m_children->rowCount(), session);
170−}
171−
172−void Session::insertChildSession(uint index, Session* session)
173−{
174− qDebug() << "Session::insertChildSession - " << session->name() << " to " << name() << " @ " << index;
175−
176− session->setParentSession(this);
177− m_children->insert(index, session);
178− SessionManager::singleton()->registerSession(session);
179−}
180−
181−void Session::removeChildSession(Session* session)
182−{
183− qDebug() << "Session::removeChildSession - " << session->name() << " from " << name();
184−
185− if (m_children->contains(session)) {
186− m_children->remove(session);
187− session->setParentSession(nullptr);
188− Q_EMIT session->deregister();
189− }
190−}
191−
192−SessionModel* Session::childSessions() const
193−{
194− return m_children;
195−}
196−
197−ObjectListModel<MirSurface>* Session::surfaces() const
198−{
199− return const_cast<ObjectListModel<MirSurface>*>(&m_surfaces);
200−}
201−
202−MirSurface *Session::lastSurface() const
203−{
204− if (m_surfaces.rowCount() > 0) {
205− return m_surfaces.list().last();
206− } else {
207− return nullptr;
208− }
209−}
tests/mocks/Unity/Application/Session.hdeleted−104
@@ -1,104 +0,0 @@
1−/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef SESSION_H
18−#define SESSION_H
19−
20−#include "ObjectListModel.h"
21−#include "SessionModel.h"
22−
23−#include <QQuickItem>
24−
25−class ApplicationInfo;
26−class MirSurface;
27−
28−class Session : public QObject
29−{
30− Q_OBJECT
31− Q_PROPERTY(QString name READ name CONSTANT)
32− Q_PROPERTY(MirSurface* lastSurface READ lastSurface NOTIFY lastSurfaceChanged)
33− Q_PROPERTY(ObjectListModel<MirSurface>* surfaces READ surfaces CONSTANT)
34− Q_PROPERTY(ApplicationInfo* application READ application NOTIFY applicationChanged)
35− Q_PROPERTY(Session* parentSession READ parentSession NOTIFY parentSessionChanged DESIGNABLE false)
36− Q_PROPERTY(SessionModel* childSessions READ childSessions DESIGNABLE false CONSTANT)
37− Q_PROPERTY(bool live READ live NOTIFY liveChanged)
38−
39−public:
40− explicit Session(const QString &name,
41− const QUrl& screenshot,
42− QObject *parent = 0);
43− ~Session();
44−
45− Q_INVOKABLE void release();
46−
47− //getters
48− QString name() const { return m_name; }
49− bool live() const { return m_live; }
50− bool fullscreen() const { return m_fullscreen; }
51− ApplicationInfo* application() const { return m_application; }
52− MirSurface *lastSurface() const;
53− ObjectListModel<MirSurface>* surfaces() const;
54− Session* parentSession() const { return m_parentSession; }
55−
56− void setApplication(ApplicationInfo* item);
57− void appendSurface(MirSurface* surface);
58− void removeSurface(MirSurface* surface);
59− void setScreenshot(const QUrl& m_screenshot);
60− void setLive(bool live);
61− void setFullscreen(bool fullscreen);
62−
63− Q_INVOKABLE void addChildSession(Session* session);
64− void insertChildSession(uint index, Session* session);
65− Q_INVOKABLE void removeChildSession(Session* session);
66−
67−Q_SIGNALS:
68− void applicationChanged(ApplicationInfo*);
69− void parentSessionChanged(Session*);
70− void liveChanged(bool live);
71− void surfaceAdded(MirSurface *surface);
72− void lastSurfaceChanged(MirSurface *surface);
73− void fullscreenChanged(bool fullscreen);
74−
75− // internal mock use
76− void deregister();
77−
78−public Q_SLOTS:
79− Q_INVOKABLE void createSurface();
80−
81−private Q_SLOTS:
82− void updateFullscreenProperty();
83−
84−private:
85− SessionModel* childSessions() const;
86− void setParentSession(Session* session);
87−
88− QString m_name;
89− bool m_live;
90− QUrl m_screenshot;
91− ApplicationInfo* m_application;
92− MirSurface* m_surface;
93− Session* m_parentSession;
94− SessionModel* m_children;
95− ObjectListModel<MirSurface> m_surfaces;
96− bool m_fullscreen;
97−
98− friend class ApplicationTestInterface;
99−};
100−
101−Q_DECLARE_METATYPE(Session*)
102−Q_DECLARE_METATYPE(ObjectListModel<MirSurface>*)
103−
104−#endif // SESSION_H
tests/mocks/Unity/Application/SessionManager.cppdeleted−55
@@ -1,55 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "SessionManager.h"
18−
19−#include "Session.h"
20−
21−SessionManager *SessionManager::the_session_manager = nullptr;
22−
23−SessionManager *SessionManager::singleton()
24−{
25− if (!the_session_manager) {
26− the_session_manager = new SessionManager();
27− }
28− return the_session_manager;
29−}
30−
31−SessionManager::SessionManager(QObject *parent) :
32− QObject(parent)
33−{
34−}
35−
36−
37−Session *SessionManager::createSession(const QString& name,
38− const QUrl& screenshot)
39−{
40− Session* session = new Session(name, screenshot);
41− Q_EMIT sessionStarting(session);
42− return session;
43−}
44−
45−void SessionManager::registerSession(Session *session)
46−{
47− connect(session, &Session::deregister, this, [this] {
48− Session* session = qobject_cast<Session*>(sender());
49−
50− disconnect(session, 0, this, 0);
51−
52− session->setLive(false);
53− Q_EMIT sessionStopping(session);
54− });
55−}
tests/mocks/Unity/Application/SessionModel.hdeleted−35
@@ -1,35 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software: you can redistribute it and/or modify it under
5− * the terms of the GNU Lesser General Public License version 3, as published by
6− * the Free Software Foundation.
7− *
8− * This program is distributed in the hope that it will be useful, but WITHOUT
9− * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10− * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11− * Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef SESSIONMODEL_H
18−#define SESSIONMODEL_H
19−
20−// Local
21−#include "ObjectListModel.h"
22−
23−class Session;
24−class SessionModel: public ObjectListModel<Session>
25−{
26− Q_OBJECT
27−public:
28− SessionModel(QObject *parent = 0): ObjectListModel(parent) {}
29−
30− Q_INVOKABLE int count() const { return rowCount(); }
31−};
32−
33−Q_DECLARE_METATYPE(SessionModel*)
34−
35−#endif // SESSIONMODEL_H
tests/mocks/Unity/Application/SurfaceManager.cpp+16−9
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,15 @@
1616
1717#include "SurfaceManager.h"
1818
19+#include "ApplicationInfo.h"
1920#include "VirtualKeyboard.h"
2021
2122#include <paths.h>
2223
2324SurfaceManager *SurfaceManager::the_surface_manager = nullptr;
2425
25−SurfaceManager *SurfaceManager::singleton()
26+SurfaceManager *SurfaceManager::instance()
2627{
27− if (!the_surface_manager) {
28− the_surface_manager = new SurfaceManager();
29− }
3028 return the_surface_manager;
3129}
3230
@@ -34,7 +32,10 @@ SurfaceManager::SurfaceManager(QObject *parent) :
3432 QObject(parent)
3533 , m_virtualKeyboard(nullptr)
3634{
37− m_virtualKeyboard = new VirtualKeyboard(this);
35+ Q_ASSERT(the_surface_manager == nullptr);
36+ the_surface_manager = this;
37+
38+ m_virtualKeyboard = new VirtualKeyboard;
3839 connect(m_virtualKeyboard, &QObject::destroyed, this, [this](QObject *obj) {
3940 MirSurface* surface = qobject_cast<MirSurface*>(obj);
4041 m_virtualKeyboard = nullptr;
@@ -43,10 +44,16 @@ SurfaceManager::SurfaceManager(QObject *parent) :
4344 });
4445}
4546
47+SurfaceManager::~SurfaceManager()
48+{
49+ Q_ASSERT(the_surface_manager == this);
50+ the_surface_manager = nullptr;
51+}
52+
4653MirSurface *SurfaceManager::createSurface(const QString& name,
47− Mir::Type type,
48− Mir::State state,
49− const QUrl& screenshot)
54+ Mir::Type type,
55+ Mir::State state,
56+ const QUrl& screenshot)
5057{
5158 MirSurface* surface = new MirSurface(name, type, state, screenshot);
5259 connect(surface, &QObject::destroyed, this, [this](QObject *obj) {
tests/mocks/Unity/Application/SurfaceManager.h+5−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@
2222#include "MirSurface.h"
2323#include "VirtualKeyboard.h"
2424
25+class ApplicationInfo;
26+
2527class SurfaceManager : public QObject
2628{
2729 Q_OBJECT
@@ -35,8 +37,9 @@ class SurfaceManager : public QObject
3537
3638public:
3739 explicit SurfaceManager(QObject *parent = 0);
40+ virtual ~SurfaceManager();
3841
39− static SurfaceManager *singleton();
42+ static SurfaceManager *instance();
4043
4144 Q_INVOKABLE MirSurface* createSurface(const QString& name,
4245 Mir::Type type,
tests/mocks/Unity/Application/UbuntuKeyboardInfo.cpp−2
@@ -16,8 +16,6 @@
1616
1717#include "UbuntuKeyboardInfo.h"
1818
19−UbuntuKeyboardInfo *UbuntuKeyboardInfo::m_instance = nullptr;
20−
2119UbuntuKeyboardInfo::UbuntuKeyboardInfo(QObject *parent)
2220 : QObject(parent),
2321 m_x(0),
tests/mocks/Unity/Application/UbuntuKeyboardInfo.h−9
@@ -40,13 +40,6 @@ public:
4040 void setWidth(qreal value);
4141 void setHeight(qreal value);
4242
43− static UbuntuKeyboardInfo *singleton() {
44− if (!m_instance) {
45− m_instance = new UbuntuKeyboardInfo;
46− }
47− return m_instance;
48− }
49−
5043Q_SIGNALS:
5144 void xChanged(qreal x);
5245 void yChanged(qreal y);
@@ -59,8 +52,6 @@ private:
5952 qint32 m_y;
6053 qint32 m_width;
6154 qint32 m_height;
62−
63− static UbuntuKeyboardInfo *m_instance;
6455};
6556
6657#endif // UBUNTU_KEYBOARD_INFO_H
tests/mocks/Unity/Application/VirtualKeyboard.cpp+2−3
@@ -22,13 +22,12 @@
2222
2323#include <QDebug>
2424
25−VirtualKeyboard::VirtualKeyboard(QObject *parent)
25+VirtualKeyboard::VirtualKeyboard()
2626 : MirSurface("input-method",
2727 Mir::InputMethodType,
2828 Mir::MinimizedState,
2929 QUrl("qrc:///Unity/Application/vkb_portrait.png"),
30− QUrl("qrc:///Unity/Application/VirtualKeyboard.qml"),
31− parent)
30+ QUrl("qrc:///Unity/Application/VirtualKeyboard.qml"))
3231{
3332}
3433
tests/mocks/Unity/Application/VirtualKeyboard.h+1−1
@@ -25,7 +25,7 @@ class VirtualKeyboard : public MirSurface
2525{
2626 Q_OBJECT
2727public:
28− explicit VirtualKeyboard(QObject *parent = nullptr);
28+ explicit VirtualKeyboard();
2929 ~VirtualKeyboard();
3030};
3131
tests/mocks/Unity/Application/plugin.cpp+32−35
@@ -17,11 +17,8 @@
1717#include "plugin.h"
1818#include "ApplicationInfo.h"
1919#include "ApplicationManager.h"
20−#include "Session.h"
2120#include "MirSurfaceItem.h"
2221#include "SurfaceManager.h"
23−#include "SessionManager.h"
24−#include "ApplicationTestInterface.h"
2522#include "UbuntuKeyboardInfo.h"
2623
2724// unity-api
@@ -30,62 +27,65 @@
3027#include <qqml.h>
3128#include <QQmlEngine>
3229
33−static QObject* applicationManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
34− Q_UNUSED(engine);
35− Q_UNUSED(scriptEngine);
36− return ApplicationManager::singleton();
37−}
30+namespace {
3831
39−static QObject* surfaceManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
40− Q_UNUSED(engine);
41− Q_UNUSED(scriptEngine);
42− return SurfaceManager::singleton();
32+// Creates the singletons that are called throughout C++ code
33+void createUnityApplicationSharedSingletons()
34+{
35+ // they have to be created in a specific order
36+ if (!MirFocusController::instance()) {
37+ new MirFocusController;
38+ }
39+ if (!SurfaceManager::instance()) {
40+ new SurfaceManager;
41+ }
4342}
4443
45−static QObject* sessionManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
46− Q_UNUSED(engine);
47− Q_UNUSED(scriptEngine);
48− return SessionManager::singleton();
44+QObject* applicationManagerSingleton(QQmlEngine*, QJSEngine*)
45+{
46+ createUnityApplicationSharedSingletons();
47+ return new ApplicationManager;
4948}
5049
51−static QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*) {
52− return UbuntuKeyboardInfo::singleton();
50+QObject* surfaceManagerSingleton(QQmlEngine*, QJSEngine*)
51+{
52+ createUnityApplicationSharedSingletons();
53+ return SurfaceManager::instance();
5354}
5455
56+QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*)
57+{
58+ createUnityApplicationSharedSingletons();
59+ return new UbuntuKeyboardInfo;
60+}
5561
56−ApplicationTestInterface* s_appTestInterface = nullptr;
57−
58−static QObject* applicationTestInterface(QQmlEngine* engine, QJSEngine* scriptEngine) {
59− Q_UNUSED(engine);
60− Q_UNUSED(scriptEngine);
61− if (!s_appTestInterface) {
62− s_appTestInterface = new ApplicationTestInterface(engine);
63− }
64− return s_appTestInterface;
62+QObject* mirFocusControllerSingleton(QQmlEngine*, QJSEngine*)
63+{
64+ createUnityApplicationSharedSingletons();
65+ return MirFocusController::instance();
6566}
6667
68+} // anonymous namespace
69+
6770void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
6871{
69− qRegisterMetaType<Session*>("Session*");
7072 qRegisterMetaType<ApplicationInfo*>("ApplicationInfo*");
71− qRegisterMetaType<SessionModel*>("SessionModel*");
7273 qRegisterMetaType<unity::shell::application::MirSurfaceInterface*>("unity::shell::application::MirSurfaceInterface*");
74+ qRegisterMetaType<unity::shell::application::MirSurfaceListInterface*>("unity::shell::application::MirSurfaceListInterface*");
7375 qRegisterMetaType<Mir::Type>("Mir::Type");
7476 qRegisterMetaType<Mir::State>("Mir::State");
7577
7678 qmlRegisterUncreatableType<unity::shell::application::ApplicationManagerInterface>(uri, 0, 1, "ApplicationManagerInterface", "Abstract interface. Cannot be created in QML");
7779 qmlRegisterUncreatableType<unity::shell::application::ApplicationInfoInterface>(uri, 0, 1, "ApplicationInfoInterface", "Abstract interface. Cannot be created in QML");
78− qmlRegisterUncreatableType<Session>(uri, 0, 1, "Session", "Session can't be instantiated from QML");
7980 qmlRegisterUncreatableType<MirSurface>(uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
8081 qmlRegisterUncreatableType<unity::shell::application::MirSurfaceInterface>(
8182 uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
83+ qmlRegisterSingletonType<MirFocusController>(uri, 0, 1, "MirFocusController", mirFocusControllerSingleton);
8284 qmlRegisterType<MirSurfaceItem>(uri, 0, 1, "MirSurfaceItem");
8385 qmlRegisterType<ApplicationInfo>(uri, 0, 1, "ApplicationInfo");
8486
8587 qmlRegisterSingletonType<ApplicationManager>(uri, 0, 1, "ApplicationManager", applicationManagerSingleton);
8688 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
87− qmlRegisterSingletonType<SessionManager>(uri, 0, 1, "SessionManager", sessionManagerSingleton);
88− qmlRegisterSingletonType<ApplicationTestInterface>(uri, 0, 1, "ApplicationTest", applicationTestInterface);
8989 qmlRegisterSingletonType<UbuntuKeyboardInfo>(uri, 0, 1, "UbuntuKeyboardInfo", ubuntuKeyboardInfoSingleton);
9090
9191 qmlRegisterUncreatableType<Mir>(uri, 0, 1, "Mir", "Mir provides enum values, it can't be instantiated");
@@ -94,7 +94,4 @@ void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
9494void FakeUnityApplicationQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
9595{
9696 QQmlExtensionPlugin::initializeEngine(engine, uri);
97−
98− // make sure we initialise our test interface.
99− applicationTestInterface(engine, nullptr);
10097}
tests/mocks/Utils/CMakeLists.txt+4
@@ -22,6 +22,10 @@ set(QMLPLUGIN_SRC
2222 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
2323 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
2424 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
25+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
26+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
27+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceListInterface.h
28+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
2529 constants.cpp
2630 plugin.cpp
2731 windowstatestorage.cpp
tests/mocks/Utils/plugin.cpp+1−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
tests/plugins/Unity/Launcher/launchermodeltest.cpp+2−3
@@ -64,6 +64,7 @@ public:
6464 void setExemptFromLifecycle(bool) override {}
6565 QSize initialSurfaceSize() const override { return QSize(); }
6666 void setInitialSurfaceSize(const QSize &) override {}
67+ MirSurfaceListInterface* surfaceList() override { return nullptr; }
6768
6869 // Methods used for mocking (not in the interface)
6970 void setFocused(bool focused) { m_focused = focused; Q_EMIT focusedChanged(focused); }
@@ -106,7 +107,7 @@ public:
106107 }
107108 return false;
108109 }
109− bool focusApplication(const QString &appId) override {
110+ bool focusApplication(const QString &appId) {
110111 Q_FOREACH(MockApp* app, m_list) {
111112 app->setFocused(app->appId() == appId);
112113 }
@@ -114,8 +115,6 @@ public:
114115 return true;
115116 }
116117
117− void unfocusCurrentApplication() override { }
118−
119118 void addApplication(MockApp *app) {
120119 beginInsertRows(QModelIndex(), count(), count());
121120 m_list.append(app);
tests/qmltests/CMakeLists.txt−1
@@ -88,7 +88,6 @@ add_unity8_qmltest(Stages DesktopStage)
8888add_unity8_qmltest(Stages PhoneStage)
8989add_unity8_qmltest(Stages SpreadDelegate)
9090add_unity8_qmltest(Stages SurfaceContainer)
91−add_unity8_qmltest(Stages SessionContainer)
9291add_unity8_qmltest(Stages TabletStage)
9392add_unity8_qmltest(Stages WindowResizeArea)
9493add_unity8_qmltest(Stages Splash)
tests/qmltests/Dash/tst_DashShell.qml+6−2
@@ -91,8 +91,12 @@ Item {
9191 var stopY = startY;
9292 touchFlick(shell, startX, startY, stopX, stopY);
9393
94− // Now do a long launcher movement
95− touchFlick(shell, stopX, startY, startX, stopY);
94+ // bring in the launcher
95+ var startX = units.gu(1);
96+ var startY = shell.height / 2;
97+ var stopX = shell.width / 3;
98+ var stopY = startY;
99+ touchFlick(shell, startX, startY, stopX, stopY);
96100
97101 var launcher = findChild(shell, "launcher");
98102 var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
tests/qmltests/Greeter/tst_Greeter.qml+2−2
@@ -361,7 +361,7 @@ Item {
361361 }
362362
363363 function test_notifyAboutToFocusApp() {
364− greeter.notifyAboutToFocusApp("fake-app");
364+ greeter.notifyUserRequestedApp("fake-app");
365365 compare(viewTryToUnlockSpy.count, 1);
366366 compare(viewTryToUnlockSpy.signalArguments[0][0], false);
367367 }
@@ -500,7 +500,7 @@ Item {
500500
501501 // Test opening a locked app
502502 greeter.lockedApp = "test-app";
503− greeter.notifyAppFocused("test-app");
503+ greeter.notifyAppFocusRequested("test-app");
504504 verify(greeter.hasLockedApp);
505505 verify(!greeter.shown);
506506
tests/qmltests/Panel/tst_ActiveCallHint.qml+3−2
@@ -96,11 +96,12 @@ Item {
9696 function test_activeHint(data) {
9797 if (data.dialer) {
9898 var application = ApplicationManager.startApplication("dialer-app");
99− ApplicationManager.focusApplication("dialer-app");
99+ tryCompare(application.surfaceList, "count", 1);
100+ MirFocusController.focusedSurface = application.surfaceList.get(0);
100101 tryCompare(ApplicationManager, "focusedApplicationId", "dialer-app");
101102 tryCompare(application, "state", ApplicationInfoInterface.Running);
102103
103− if (!data.focused) { ApplicationManager.unfocusCurrentApplication(); }
104+ if (!data.focused) { MirFocusController.focusedSurface = null; }
104105 }
105106 callManager.foregroundCall = data.call;
106107 callManager.callIndicatorVisible = data.visible;
tests/qmltests/Panel/tst_Panel.qml+2−2
@@ -29,7 +29,7 @@ IndicatorTest {
2929 id: root
3030 width: units.gu(100)
3131 height: units.gu(71)
32− color: "white"
32+ color: "black"
3333
3434 Component.onCompleted: theme.name = "Ubuntu.Components.Themes.SuruDark"
3535
@@ -464,7 +464,7 @@ IndicatorTest {
464464
465465 callManager.foregroundCall = phoneCall;
466466
467− ApplicationManager.focusApplication("unity8-dash");
467+ MirFocusController.focusedSurface = ApplicationManager.findApplication("unity8-dash").surfaceList.get(0);
468468 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
469469
470470 mouseClick(panel.indicators,
tests/qmltests/Stages/ApplicationCheckBox.qml+104−58
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -19,11 +19,17 @@ import QtQuick.Layouts 1.1
1919import Ubuntu.Components 1.3
2020import Unity.Application 0.1
2121
22−RowLayout {
22+Column {
2323 id: root
2424 property string appId
2525 property bool checked: false
2626
27+ function createSurface() {
28+ if (d.application) {
29+ d.application.createSurface();
30+ }
31+ }
32+
2733 enabled: appId !== "unity8-dash"
2834
2935 onCheckedChanged: {
@@ -38,77 +44,117 @@ RowLayout {
3844 d.bindGuard = false;
3945 }
4046
41− QtObject {
42− id: d
43− property bool bindGuard: false
44− property var application: null
45− Component.onCompleted: {
46− application = ApplicationManager.findApplication(root.appId);
47+ RowLayout {
48+
49+ QtObject {
50+ id: d
51+ property bool bindGuard: false
52+ property var application: null
53+ Component.onCompleted: {
54+ application = ApplicationManager.findApplication(root.appId);
55+ }
4756 }
48− }
4957
50− Connections {
51− target: ApplicationManager
52− onCountChanged: {
53− d.application = ApplicationManager.findApplication(root.appId);
58+ Connections {
59+ target: ApplicationManager
60+ onCountChanged: {
61+ d.application = ApplicationManager.findApplication(root.appId);
62+ }
5463 }
55− }
5664
57− Layout.fillWidth: true
58− CheckBox {
59− id: checkbox
60− checked: false
61− activeFocusOnPress: false
65+ Layout.fillWidth: true
66+ CheckBox {
67+ id: checkbox
68+ checked: false
69+ activeFocusOnPress: false
6270
63− onTriggered: {
64− if (d.bindGuard) { return; }
65− d.bindGuard = true;
71+ onTriggered: {
72+ if (d.bindGuard) { return; }
73+ d.bindGuard = true;
6674
67− if (checked) {
68− ApplicationManager.startApplication(root.appId);
69− } else {
70− ApplicationManager.stopApplication(root.appId);
75+ if (checked) {
76+ ApplicationManager.startApplication(root.appId);
77+ } else {
78+ ApplicationManager.stopApplication(root.appId);
79+ }
80+ d.bindGuard = false;
7181 }
72− d.bindGuard = false;
73− }
74− onCheckedChanged: {
75− if (d.bindGuard) { return; }
76− d.bindGuard = true;
82+ onCheckedChanged: {
83+ if (d.bindGuard) { return; }
84+ d.bindGuard = true;
7785
78− root.checked = checked;
86+ root.checked = checked;
7987
80− d.bindGuard = false;
88+ d.bindGuard = false;
89+ }
90+ Binding {
91+ target: checkbox
92+ property: "checked"
93+ value: d.application != null
94+ }
8195 }
82− Binding {
83− target: checkbox
84− property: "checked"
85− value: d.application != null
96+ Label {
97+ id: appIdLabel
98+ text: root.appId
99+ color: "white"
100+ anchors.verticalCenter: parent.verticalCenter
86101 }
87− }
88− Label {
89− id: appIdLabel
90− text: root.appId
91− color: "white"
92− anchors.verticalCenter: parent.verticalCenter
93− }
94− Rectangle {
95− color: {
96− if (d.application) {
97− if (d.application.state === ApplicationInfoInterface.Starting) {
98− return "yellow";
99− } else if (d.application.state === ApplicationInfoInterface.Running) {
100− return "green";
101− } else if (d.application.state === ApplicationInfoInterface.Suspended) {
102− return "blue";
102+ Rectangle {
103+ color: {
104+ if (d.application) {
105+ if (d.application.state === ApplicationInfoInterface.Starting) {
106+ return "yellow";
107+ } else if (d.application.state === ApplicationInfoInterface.Running) {
108+ return "green";
109+ } else if (d.application.state === ApplicationInfoInterface.Suspended) {
110+ return "blue";
111+ } else {
112+ return "darkred";
113+ }
103114 } else {
104115 return "darkred";
105116 }
106− } else {
107− return "darkred";
117+ }
118+ width: height
119+ height: appIdLabel.height * 0.7
120+ anchors.verticalCenter: parent.verticalCenter
121+ }
122+
123+ MouseArea {
124+ width: height
125+ height: appIdLabel.height * 0.7
126+ anchors.verticalCenter: parent.verticalCenter
127+ onClicked: root.createSurface()
128+ enabled: d.application && d.application.state === ApplicationInfoInterface.Running
129+ visible: enabled
130+ Label {
131+ text: "➕"
132+ color: "white"
133+ anchors.centerIn: parent
134+ }
135+ }
136+ }
137+ Repeater {
138+ model: d.application ? d.application.surfaceList : null
139+ RowLayout {
140+ spacing: units.gu(1)
141+ Label {
142+ color: "white"
143+ text: "- " + model.surface.name
144+ }
145+ MouseArea {
146+ width: height
147+ height: appIdLabel.height * 0.7
148+ anchors.verticalCenter: parent.verticalCenter
149+ enabled: model.surface.live
150+ visible: enabled
151+ onClicked: model.surface.setLive(false);
152+ Label {
153+ text: "⛒"
154+ color: "white"
155+ anchors.centerIn: parent
156+ }
108157 }
109158 }
110− width: height
111− height: appIdLabel.height * 0.7
112− anchors.verticalCenter: parent.verticalCenter
113159 }
114160}
tests/qmltests/Stages/RecursingChildSessionControl.qml+10−61
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014,2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,7 @@ import Unity.Application 0.1
2222ColumnLayout {
2323 id: root
2424
25− property var session
26− property var childSessions: session ? session.childSessions : 0
27− property bool removable: false
28− property alias surfaceCheckbox: _surfaceCheckbox
29−
30− property var screenshotIds: [ "gallery", "map", "facebook", "camera", "browser", "music", "twitter"]
31−
32− onSessionChanged: {
33− if (!session) surfaceCheckbox.checked = false;
34− }
25+ property var surface
3526
3627 Column {
3728 id: column
@@ -41,7 +32,7 @@ ColumnLayout {
4132
4233 Repeater {
4334 id: repeater
44− model: root.childSessions
35+ model: root.surface ? root.surface.promptSurfaceList : null
4536 delegate: Rectangle {
4637 border {
4738 color: "black"
@@ -71,8 +62,7 @@ ColumnLayout {
7162 }
7263
7364 onLoaded: {
74− item.session = modelData;
75− item.removable = true;
65+ item.surface = model.surface;
7666 }
7767 }
7868 }
@@ -83,62 +73,21 @@ ColumnLayout {
8373 Layout.fillWidth: true
8474 spacing: units.gu(1)
8575
86− RowLayout {
87− Layout.fillWidth: true
88−
89− CheckBox {
90− id: _surfaceCheckbox;
91− checked: root.session.lastSurface ? true : false
92− activeFocusOnPress: false
93− enabled: root.session
94− onCheckedChanged: {
95− if (checked) {
96− root.session.createSurface();
97− } else if (root.session && root.session.lastSurface) {
98− ApplicationTest.removeSurface(root.session.lastSurface);
99− }
100− }
101−
102− Connections {
103− target: root.session ? root.session : null
104− onLastSurfaceChanged: {
105− surfaceCheckbox.checked = root.session.lastSurface !== null
106− }
107− }
108− }
109−
110− Label {
111− text: "Surface"
112− anchors.verticalCenter: parent.verticalCenter
113− }
114− }
115−
11676 RowLayout {
11777 id: buttonLayout
11878
11979 Button {
120− enabled: root.session
80+ enabled: root.surface
12181 activeFocusOnPress: false
122− text: removable ? "Remove" : "Release"
123− onClicked: {
124− if (removable) {
125− // release the surface first. simulates mir app closing
126− if (root.session.lastSurface) ApplicationTest.removeSurface(root.session.lastSurface);
127− ApplicationTest.removeSession(root.session);
128− } else {
129− root.session.release();
130− }
131− }
82+ text: "Remove"
83+ onClicked: { root.surface.close(); }
13284 }
13385
13486 Button {
135− enabled: root.session !== null
87+ enabled: root.surface !== null
13688 activeFocusOnPress: false
137− text: "Add Child"
138− onClicked: {
139− var screenshot = Math.round(Math.random()*100 % (screenshotIds.length-1));
140− var session = ApplicationTest.addChildSession(root.session, root.screenshotIds[screenshot], true);
141− }
89+ text: "Add Prompt Surface"
90+ onClicked: { root.surface.createPromptSurface(); }
14291 }
14392 }
14493 }
tests/qmltests/Stages/tst_ApplicationWindow.qml+73−59
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -34,16 +34,9 @@ Rectangle {
3434 root.fakeApplication = ApplicationManager.add("gallery-app");
3535 root.fakeApplication.manualSurfaceCreation = true;
3636 root.fakeApplication.setState(ApplicationInfoInterface.Starting);
37+ applicationWindowLoader.item.application = root.fakeApplication;
3738 }
3839 property QtObject fakeApplication: null
39− readonly property var fakeSession: fakeApplication ? fakeApplication.session : null
40−
41− Connections {
42− target: fakeApplication
43− onSessionChanged: {
44− sessionCheckbox.checked = fakeApplication.session !== null
45− }
46− }
4740
4841 Loader {
4942 id: applicationWindowLoader
@@ -58,7 +51,6 @@ Rectangle {
5851 sourceComponent: Component {
5952 ApplicationWindow {
6053 anchors.fill: parent
61− application: fakeApplication
6254 surfaceOrientationAngle: 0
6355 interactive: true
6456 focus: true
@@ -86,18 +78,27 @@ Rectangle {
8678 Layout.fillWidth: true
8779
8880 CheckBox {
89− id: sessionCheckbox; checked: false
81+ id: surfaceCheckbox;
82+ checked: false;
9083 activeFocusOnPress: false
9184 onCheckedChanged: {
92− if (checked && !fakeApplication.session) {
93− fakeApplication.createSession();
94− } else if (!checked && fakeApplication.session) {
95− fakeApplication.destroySession();
85+ if (applicationWindowLoader.status !== Loader.Ready)
86+ return;
87+
88+ if (checked) {
89+ applicationWindowLoader.item.surface = SurfaceManager.createSurface("foo",
90+ Mir.NormalType, Mir.MaximizedState, root.fakeApplication);
91+
92+ root.fakeApplication.setState(ApplicationInfoInterface.Running);
93+ } else {
94+ if (applicationWindowLoader.item.surface) {
95+ applicationWindowLoader.item.surface.setLive(false);
96+ }
9697 }
9798 }
9899 }
99100 Label {
100− text: "Session"
101+ text: "Surface"
101102 anchors.verticalCenter: parent.verticalCenter
102103 }
103104 }
@@ -117,7 +118,7 @@ Rectangle {
117118 id: sessionControl
118119 anchors { left: parent.left; right: parent.right; }
119120
120− session: fakeSession
121+ surface: applicationWindowLoader.item ? applicationWindowLoader.item.surface : null
121122 }
122123 }
123124
@@ -143,7 +144,15 @@ Rectangle {
143144 }
144145 onSelectedApplicationStateChanged: {
145146 // state is a read-only property, thus we have to call the setter function
146− fakeApplication.setState(selectedApplicationState);
147+ if (fakeApplication && fakeApplication.state != selectedApplicationState) {
148+ fakeApplication.setState(selectedApplicationState);
149+ }
150+ }
151+ Connections {
152+ target: fakeApplication
153+ onStateChanged: {
154+ testCase.setApplicationState(state);
155+ }
147156 }
148157 }
149158
@@ -197,6 +206,8 @@ Rectangle {
197206 }
198207 }
199208
209+ property var applicationWindow: applicationWindowLoader.item
210+
200211 // holds some of the internal ApplicationWindow objects we probe during the tests
201212 property var stateGroup: null
202213
@@ -213,11 +224,6 @@ Rectangle {
213224 findInterestingApplicationWindowChildren();
214225 }
215226
216− function initSession() {
217− sessionCheckbox.checked = true;
218− sessionControl.surfaceCheckbox.checked = true;
219− }
220−
221227 function cleanup() {
222228 forgetApplicationWindowChildren();
223229
@@ -238,10 +244,17 @@ Rectangle {
238244 // deleted, so you end up with two instances in memory.
239245 tryCompare(applicationWindowLoader, "itemDestroyed", true);
240246
241− appStateSelector.selectedIndex = 0;
242− sessionCheckbox.checked = false;
247+ setApplicationState(appStarting);
248+ surfaceCheckbox.checked = false;
249+
250+ ApplicationManager.stopApplication("gallery-app");
251+ root.fakeApplication = ApplicationManager.add("gallery-app");
252+ root.fakeApplication.manualSurfaceCreation = true;
253+ root.fakeApplication.setState(ApplicationInfoInterface.Starting);
243254
244255 applicationWindowLoader.active = true;
256+
257+ applicationWindowLoader.item.application = root.fakeApplication;
245258 }
246259
247260 function test_showSplashUntilAppFullyInit_data() {
@@ -256,7 +269,7 @@ Rectangle {
256269 verify(stateGroup.state === "splashScreen");
257270
258271 if (data.swapInitOrder) {
259− initSession();
272+ surfaceCheckbox.checked = true;
260273 } else {
261274 setApplicationState(appRunning);
262275 }
@@ -266,14 +279,14 @@ Rectangle {
266279 if (data.swapInitOrder) {
267280 setApplicationState(appRunning);
268281 } else {
269− initSession();
282+ surfaceCheckbox.checked = true;
270283 }
271284
272285 tryCompare(stateGroup, "state", "surface");
273286 }
274287
275288 function test_suspendedAppShowsSurface() {
276− initSession();
289+ surfaceCheckbox.checked = true;
277290 setApplicationState(appRunning);
278291
279292 tryCompare(stateGroup, "state", "surface");
@@ -287,7 +300,7 @@ Rectangle {
287300 }
288301
289302 function test_killedAppShowsScreenshot() {
290− initSession();
303+ surfaceCheckbox.checked = true;
291304 setApplicationState(appRunning);
292305 tryCompare(stateGroup, "state", "surface");
293306
@@ -297,16 +310,17 @@ Rectangle {
297310 verify(fakeApplication.surface !== null);
298311
299312 // kill it!
313+ surfaceCheckbox.checked = false;
300314 setApplicationState(appStopped);
301315
302316 tryCompare(stateGroup, "state", "screenshot");
303− tryCompare(fakeApplication, "session", null);
317+ tryCompare(fakeApplication.surfaceList, "count", 0);
304318 }
305319
306320 function test_restartApp() {
307− var screenshotImage = findChild(applicationWindowLoader.item, "screenshotImage");
321+ var screenshotImage = findChild(applicationWindow, "screenshotImage");
308322
309− initSession();
323+ surfaceCheckbox.checked = true;
310324 setApplicationState(appRunning);
311325 tryCompare(stateGroup, "state", "surface");
312326 waitUntilTransitionsEnd(stateGroup);
@@ -314,109 +328,109 @@ Rectangle {
314328 setApplicationState(appSuspended);
315329
316330 // kill it
331+ surfaceCheckbox.checked = false;
317332 setApplicationState(appStopped);
318333
319334 tryCompare(stateGroup, "state", "screenshot");
320335 waitUntilTransitionsEnd(stateGroup);
321− tryCompare(fakeApplication, "session", null);
336+ tryCompare(applicationWindow, "surface", null);
322337
323338 // and restart it
324339 setApplicationState(appStarting);
325340
326341 waitUntilTransitionsEnd(stateGroup);
327342 verify(stateGroup.state === "screenshot");
328− verify(fakeSession === null);
343+ verify(applicationWindow.surface === null);
329344
330345 setApplicationState(appRunning);
331346
332347 waitUntilTransitionsEnd(stateGroup);
333348 verify(stateGroup.state === "screenshot");
334349
335− initSession();
350+ surfaceCheckbox.checked = true;
336351
337352 tryCompare(stateGroup, "state", "surface");
338353 tryCompare(screenshotImage, "status", Image.Null);
339354 }
340355
341356 function test_appCrashed() {
342− initSession();
357+ surfaceCheckbox.checked = true;
343358 setApplicationState(appRunning);
344359 tryCompare(stateGroup, "state", "surface");
345360 waitUntilTransitionsEnd(stateGroup);
346361
347362 // oh, it crashed...
363+ surfaceCheckbox.checked = false;
348364 setApplicationState(appStopped);
349365
350366 tryCompare(stateGroup, "state", "screenshot");
351− tryCompare(fakeApplication, "session", null);
367+ tryCompare(applicationWindow, "surface", null);
352368 }
353369
354370 function test_keepSurfaceWhileInvisible() {
355− initSession();
371+ surfaceCheckbox.checked = true;
356372 setApplicationState(appRunning);
357373 tryCompare(stateGroup, "state", "surface");
358374 waitUntilTransitionsEnd(stateGroup);
359− verify(fakeSession.lastSurface !== null);
375+ verify(applicationWindow.surface !== null);
360376
361− applicationWindowLoader.item.visible = false;
377+ applicationWindow.visible = false;
362378
363379 waitUntilTransitionsEnd(stateGroup);
364380 verify(stateGroup.state === "surface");
365− verify(fakeSession.lastSurface !== null);
381+ verify(applicationWindow.surface !== null);
366382
367− applicationWindowLoader.item.visible = true;
383+ applicationWindow.visible = true;
368384
369385 waitUntilTransitionsEnd(stateGroup);
370386 verify(stateGroup.state === "surface");
371− verify(fakeSession.lastSurface !== null);
387+ verify(applicationWindow.surface !== null);
372388 }
373389
374390 function test_touchesReachSurfaceWhenItsShown() {
375391 setApplicationState(appRunning);
376− initSession();
392+ surfaceCheckbox.checked = true;
377393
378394 tryCompare(stateGroup, "state", "surface");
379395
380396 waitUntilTransitionsEnd(stateGroup);
381397
382− var surfaceItem = findChild(applicationWindowLoader.item, "surfaceItem");
398+ var surfaceItem = findChild(applicationWindow, "surfaceItem");
383399 verify(surfaceItem);
384− verify(surfaceItem.surface === fakeSession.lastSurface);
400+ verify(surfaceItem.surface === applicationWindow.surface);
385401
386402 surfaceItem.touchPressCount = 0;
387403 surfaceItem.touchReleaseCount = 0;
388404
389− tap(applicationWindowLoader.item,
390− applicationWindowLoader.item.width / 2, applicationWindowLoader.item.height / 2);
405+ tap(applicationWindow);
391406
392407 verify(surfaceItem.touchPressCount === 1);
393408 verify(surfaceItem.touchReleaseCount === 1);
394409 }
395410
396411 function test_showNothingOnSuddenSurfaceLoss() {
397− initSession();
412+ surfaceCheckbox.checked = true;
398413 setApplicationState(appRunning);
399414 tryCompare(stateGroup, "state", "surface");
400415 waitUntilTransitionsEnd(stateGroup);
401416
402− sessionControl.surfaceCheckbox.checked = false;
417+ applicationWindow.surface = null;
403418
404419 tryCompare(stateGroup, "state", "void");
405420 }
406421
407422 function test_surfaceActiveFocusFollowsAppWindowInterative() {
408− fakeApplication.createSession();
409− applicationWindowLoader.item.interactive = false;
410− applicationWindowLoader.item.interactive = true;
411− fakeSession.createSurface();
423+ applicationWindow.interactive = false;
424+ applicationWindow.interactive = true;
425+ surfaceCheckbox.checked = true;
412426
413− compare(fakeSession.lastSurface.activeFocus, true);
427+ compare(applicationWindow.surface.activeFocus, true);
414428
415− applicationWindowLoader.item.interactive = false;
416− compare(fakeSession.lastSurface.activeFocus, false);
429+ applicationWindow.interactive = false;
430+ compare(applicationWindow.surface.activeFocus, false);
417431
418− applicationWindowLoader.item.interactive = true;
419− compare(fakeSession.lastSurface.activeFocus, true);
432+ applicationWindow.interactive = true;
433+ compare(applicationWindow.surface.activeFocus, true);
420434 }
421435 }
422436}
tests/qmltests/Stages/tst_DesktopStage.qml+243−161
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
2020import Ubuntu.Components.ListItems 1.3
2121import Unity.Application 0.1
2222import Unity.Test 0.1
23+import WindowManager 0.1
2324import Utils 0.1
2425
2526import ".." // For EdgeBarrierControls
@@ -53,6 +54,11 @@ Item {
5354 }
5455 }
5556
57+ TopLevelSurfaceList {
58+ id: topSurfaceList
59+ applicationsModel: ApplicationManager
60+ }
61+
5662 Loader {
5763 id: desktopStageLoader
5864 x: ((root.width - controls.width) - width) / 2
@@ -68,6 +74,7 @@ Item {
6874 color: "white"
6975 anchors.fill: parent
7076 background: "../../../qml/graphics/tablet_background.jpg"
77+ focus: true
7178
7279 Component.onCompleted: {
7380 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
@@ -76,6 +83,8 @@ Item {
7683 desktopStageLoader.itemDestroyed = true;
7784 }
7885 orientations: Orientations {}
86+ applicationManager: ApplicationManager
87+ topLevelSurfaceList: topSurfaceList
7988 }
8089 }
8190 }
@@ -103,7 +112,7 @@ Item {
103112 activeFocusOnPress: false
104113 onClicked: {
105114 if (ApplicationManager.focusedApplicationId) {
106− var surface = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).session.lastSurface;
115+ var surface = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).surfaceList.get(0);
107116 surface.slowToResize = true;
108117 }
109118 }
@@ -137,6 +146,19 @@ Item {
137146
138147 property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
139148
149+ function init() {
150+ // wait until unity8-dash is up and running.
151+ // it's started automatically by ApplicationManager mock implementation
152+ tryCompare(ApplicationManager, "count", 1);
153+ var dashApp = ApplicationManager.findApplication("unity8-dash");
154+ verify(dashApp);
155+ tryCompare(dashApp, "state", ApplicationInfoInterface.Running);
156+
157+ tryCompare(topSurfaceList, "count", 1);
158+ tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) != null;}, true);
159+ compare(MirFocusController.focusedSurface, topSurfaceList.surfaceAt(0));
160+ }
161+
140162 function cleanup() {
141163 desktopStageLoader.itemDestroyed = false;
142164 desktopStageLoader.active = false;
@@ -149,43 +171,71 @@ Item {
149171 // Shell instance gets destroyed.
150172 tryCompare(desktopStageLoader, "itemDestroyed", true);
151173
152− killAllRunningApps();
174+ killApps();
175+
176+ root.resetGeometry();
153177
154178 desktopStageLoader.active = true;
155179 tryCompare(desktopStageLoader, "status", Loader.Ready);
156− root.resetGeometry();
157− }
158180
159− function killAllRunningApps() {
160− while (ApplicationManager.count > 0) {
161− ApplicationManager.stopApplication(ApplicationManager.get(0).appId);
162− }
163− compare(ApplicationManager.count, 0)
164181 }
165182
166− function waitUntilAppSurfaceShowsUp(appId) {
167− var appWindow = findChild(desktopStage, "appWindow_" + appId);
183+ function waitUntilAppSurfaceShowsUp(surfaceId) {
184+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
185+ verify(appDelegate);
186+ var appWindow = findChild(appDelegate, "appWindow");
168187 verify(appWindow);
169188 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
170189 verify(appWindowStates);
171190 tryCompare(appWindowStates, "state", "surface");
172191 }
173192
193+ /*
194+ Returns the appDelegate of the first surface created by the app with the specified appId
195+ */
174196 function startApplication(appId) {
175− var app = ApplicationManager.findApplication(appId);
176− if (!app) {
197+ try {
198+ var app = ApplicationManager.findApplication(appId);
199+ if (app) {
200+ for (var i = 0; i < topSurfaceList.count; i++) {
201+ if (topSurfaceList.applicationAt(i).appId === appId) {
202+ var appRepeater = findChild(desktopStage, "appRepeater");
203+ verify(appRepeater);
204+ return appRepeater.itemAt(i);
205+ }
206+ }
207+ }
208+
209+ var surfaceId = topSurfaceList.nextId;
177210 app = ApplicationManager.startApplication(appId);
211+ verify(app);
212+ waitUntilAppSurfaceShowsUp(surfaceId);
213+ compare(app.surfaceList.count, 1);
214+
215+ return findChild(desktopStage, "appDelegate_" + surfaceId);
216+ } catch(err) {
217+ throw new Error("startApplication("+appId+") called from line " + util.callerLine(1) + " failed!");
218+ }
219+ }
220+
221+ function maximizeAppDelegate(appDelegate) {
222+ var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
223+ if (!maximizeButton) {
224+ fail("Could not find maximizeWindowButton of appDelegate=" + appDelegate);
225+ }
226+ mouseClick(maximizeButton);
227+
228+ try {
229+ tryCompare(appDelegate, "visuallyMaximized", true);
230+ } catch(err) {
231+ fail("appDelegate.visuallyMaximized !== true");
178232 }
179− verify(app);
180− waitUntilAppSurfaceShowsUp(appId);
181− verify(app.session.lastSurface);
182− return app;
183233 }
184234
185235 function test_appFocusSwitch_data() {
186236 return [
187− {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 0, focusTo: 1 },
188− {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 1, focusTo: 0 },
237+ {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "gmail-webapp" ], focusfrom: 0, focusTo: 1 },
238+ {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "gmail-webapp" ], focusfrom: 1, focusTo: 0 },
189239 ]
190240 }
191241
@@ -193,10 +243,10 @@ Item {
193243 data.apps.forEach(startApplication);
194244
195245 ApplicationManager.requestFocusApplication(data.apps[data.focusfrom]);
196− tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.lastSurface, "activeFocus", true);
246+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).surfaceList.get(0), "activeFocus", true);
197247
198248 ApplicationManager.requestFocusApplication(data.apps[data.focusTo]);
199− tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.lastSurface, "activeFocus", true);
249+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).surfaceList.get(0), "activeFocus", true);
200250 }
201251
202252 function test_tappingOnWindowChangesFocusedApp_data() {
@@ -207,21 +257,24 @@ Item {
207257 }
208258
209259 function test_tappingOnWindowChangesFocusedApp(data) {
210− data.apps.forEach(startApplication);
211− var fromAppId = data.apps[data.focusfrom];
212− var toAppId = data.apps[data.focusTo]
260+ var appDelegates = [];
261+ for (var i = 0; i < data.apps.length; i++) {
262+ appDelegates[i] = startApplication(data.apps[i]);
263+ }
264+ var fromDelegate = appDelegates[data.focusfrom];
265+ var toDelegate = appDelegates[data.focusTo];
213266
214− var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
267+ var fromAppWindow = findChild(fromDelegate, "appWindow");
215268 verify(fromAppWindow);
216269 tap(fromAppWindow);
217− compare(fromAppWindow.application.session.lastSurface.activeFocus, true);
218− compare(ApplicationManager.focusedApplicationId, fromAppId);
270+ compare(fromDelegate.surface.activeFocus, true);
271+ compare(MirFocusController.focusedSurface, fromDelegate.surface);
219272
220− var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
273+ var toAppWindow = findChild(toDelegate, "appWindow");
221274 verify(toAppWindow);
222275 tap(toAppWindow);
223− compare(toAppWindow.application.session.lastSurface.activeFocus, true);
224− compare(ApplicationManager.focusedApplicationId, toAppId);
276+ compare(toDelegate.surface.activeFocus, true);
277+ compare(MirFocusController.focusedSurface, toDelegate.surface);
225278 }
226279
227280 function test_clickingOnWindowChangesFocusedApp_data() {
@@ -229,21 +282,24 @@ Item {
229282 }
230283
231284 function test_clickingOnWindowChangesFocusedApp(data) {
232− data.apps.forEach(startApplication);
233− var fromAppId = data.apps[data.focusfrom];
234− var toAppId = data.apps[data.focusTo]
285+ var appDelegates = [];
286+ for (var i = 0; i < data.apps.length; i++) {
287+ appDelegates[i] = startApplication(data.apps[i]);
288+ }
289+ var fromDelegate = appDelegates[data.focusfrom];
290+ var toDelegate = appDelegates[data.focusTo];
235291
236− var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
292+ var fromAppWindow = findChild(fromDelegate, "appWindow");
237293 verify(fromAppWindow);
238294 mouseClick(fromAppWindow);
239− compare(fromAppWindow.application.session.lastSurface.activeFocus, true);
240− compare(ApplicationManager.focusedApplicationId, fromAppId);
295+ compare(fromDelegate.surface.activeFocus, true);
296+ compare(MirFocusController.focusedSurface, fromDelegate.surface);
241297
242− var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
298+ var toAppWindow = findChild(toDelegate, "appWindow");
243299 verify(toAppWindow);
244300 mouseClick(toAppWindow);
245− compare(toAppWindow.application.session.lastSurface.activeFocus, true);
246− compare(ApplicationManager.focusedApplicationId, toAppId);
301+ compare(toDelegate.surface.activeFocus, true);
302+ compare(MirFocusController.focusedSurface, toDelegate.surface);
247303 }
248304
249305 function test_tappingOnDecorationFocusesApplication_data() {
@@ -253,24 +309,49 @@ Item {
253309 ]
254310 }
255311
312+ function findDecoratedWindow(surfaceId) {
313+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
314+ if (!appDelegate) {
315+ console.warn("Could not find appDelegate for surfaceId="+surfaceId);
316+ return null;
317+ }
318+ return findChild(appDelegate, "decoratedWindow");
319+ }
320+
321+ function findWindowDecoration(surfaceId) {
322+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
323+ if (!appDelegate) {
324+ console.warn("Could not find appDelegate for surfaceId="+surfaceId);
325+ return null;
326+ }
327+ return findChild(appDelegate, "appWindowDecoration");
328+ }
329+
256330 function test_tappingOnDecorationFocusesApplication(data) {
257− data.apps.forEach(startApplication);
331+ var appDelegates = [];
332+ for (var i = 0; i < data.apps.length; i++) {
333+ appDelegates[i] = startApplication(data.apps[i]);
334+ }
335+
336+ var fromDelegate = appDelegates[data.focusfrom];
337+ var toDelegate = appDelegates[data.focusTo];
258338
259− var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
339+ var fromAppDecoration = findChild(fromDelegate, "appWindowDecoration");
260340 verify(fromAppDecoration);
261341 tap(fromAppDecoration);
262342
263− var fromApp = ApplicationManager.findApplication(data.apps[data.focusfrom]);
264− verify(fromApp);
265− tryCompare(fromApp.session.lastSurface, "activeFocus", true);
343+ tryCompare(fromDelegate.surface, "activeFocus", true);
266344
267− var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
345+ var toAppDecoration = findChild(toDelegate, "appWindowDecoration");
268346 verify(toAppDecoration);
347+
348+ // FIXME: Wait a bit before the second tap or the window decoration will say it got double clicked.
349+ // No idea why.
350+ wait(700);
351+
269352 tap(toAppDecoration);
270353
271− var toApp = ApplicationManager.findApplication(data.apps[data.focusTo]);
272− verify(toApp);
273− tryCompare(toApp.session.lastSurface, "activeFocus", true);
354+ tryCompare(toDelegate.surface, "activeFocus", true);
274355 }
275356
276357 function test_clickingOnDecorationFocusesApplication_data() {
@@ -278,159 +359,170 @@ Item {
278359 }
279360
280361 function test_clickingOnDecorationFocusesApplication(data) {
281− data.apps.forEach(startApplication);
362+ var appDelegates = [];
363+ for (var i = 0; i < data.apps.length; i++) {
364+ appDelegates[i] = startApplication(data.apps[i]);
365+ }
366+ var fromDelegate = appDelegates[data.focusfrom];
367+ var toDelegate = appDelegates[data.focusTo];
282368
283− var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
369+ var fromAppDecoration = findChild(fromDelegate, "appWindowDecoration");
284370 verify(fromAppDecoration);
285371 mouseClick(fromAppDecoration);
286− tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.lastSurface, "activeFocus", true);
372+ tryCompare(fromDelegate.surface, "activeFocus", true);
287373
288− var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
374+ var toAppDecoration = findChild(toDelegate, "appWindowDecoration");
289375 verify(toAppDecoration);
290376 mouseClick(toAppDecoration);
291− tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.lastSurface, "activeFocus", true);
377+ tryCompare(toDelegate.surface, "activeFocus", true);
292378 }
293379
294380 function test_windowMaximize() {
295− var apps = ["unity8-dash", "dialer-app", "camera-app"];
296− apps.forEach(startApplication);
297− var appName = "dialer-app";
298− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
299− verify(appDelegate);
300− ApplicationManager.focusApplication(appName);
381+ var dialerDelegate = startApplication("dialer-app");
382+ startApplication("camera-app");
383+
384+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
385+ dialerDelegate.surface.requestFocus();
386+ tryCompare(dialerDelegate, "focus", true);
387+
301388 keyClick(Qt.Key_Up, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Up shortcut to maximize
302− tryCompare(appDelegate, "maximized", true);
303− tryCompare(appDelegate, "minimized", false);
389+ tryCompare(dialerDelegate, "maximized", true);
390+ tryCompare(dialerDelegate, "minimized", false);
304391 }
305392
306393 function test_windowMaximizeLeft() {
307− var apps = ["unity8-dash", "dialer-app", "camera-app"];
308− apps.forEach(startApplication);
309− var appName = "dialer-app";
310− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
311− verify(appDelegate);
312− ApplicationManager.focusApplication(appName);
394+ var dialerDelegate = startApplication("dialer-app");
395+ startApplication("camera-app");
396+
397+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
398+ dialerDelegate.surface.requestFocus();
399+ tryCompare(dialerDelegate, "focus", true);
400+
313401 keyClick(Qt.Key_Left, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Left shortcut to maximizeLeft
314− tryCompare(appDelegate, "maximized", false);
315− tryCompare(appDelegate, "minimized", false);
316− tryCompare(appDelegate, "maximizedLeft", true);
317− tryCompare(appDelegate, "maximizedRight", false);
402+ tryCompare(dialerDelegate, "maximized", false);
403+ tryCompare(dialerDelegate, "minimized", false);
404+ tryCompare(dialerDelegate, "maximizedLeft", true);
405+ tryCompare(dialerDelegate, "maximizedRight", false);
318406 }
319407
320408 function test_windowMaximizeRight() {
321− var apps = ["unity8-dash", "dialer-app", "camera-app"];
322− apps.forEach(startApplication);
323− var appName = "dialer-app";
324− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
325− verify(appDelegate);
326− ApplicationManager.focusApplication(appName);
409+ var dialerDelegate = startApplication("dialer-app");
410+ startApplication("camera-app");
411+
412+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
413+ dialerDelegate.surface.requestFocus();
414+ tryCompare(dialerDelegate, "focus", true);
415+
327416 keyClick(Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Right shortcut to maximizeRight
328− tryCompare(appDelegate, "maximized", false);
329− tryCompare(appDelegate, "minimized", false);
330− tryCompare(appDelegate, "maximizedLeft", false);
331− tryCompare(appDelegate, "maximizedRight", true);
417+ tryCompare(dialerDelegate, "maximized", false);
418+ tryCompare(dialerDelegate, "minimized", false);
419+ tryCompare(dialerDelegate, "maximizedLeft", false);
420+ tryCompare(dialerDelegate, "maximizedRight", true);
332421 }
333422
334423 function test_windowMinimize() {
335− var apps = ["unity8-dash", "dialer-app", "camera-app"];
336− apps.forEach(startApplication);
337− var appName = "dialer-app";
338− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
339− verify(appDelegate);
340− ApplicationManager.focusApplication(appName);
424+ var dialerDelegate = startApplication("dialer-app");
425+ startApplication("camera-app");
426+
427+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
428+ dialerDelegate.surface.requestFocus();
429+ tryCompare(dialerDelegate, "focus", true);
430+
341431 keyClick(Qt.Key_Down, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Down shortcut to minimize
342− tryCompare(appDelegate, "maximized", false);
343− tryCompare(appDelegate, "minimized", true);
432+ tryCompare(dialerDelegate, "maximized", false);
433+ tryCompare(dialerDelegate, "minimized", true);
344434 verify(ApplicationManager.focusedApplicationId != ""); // verify we don't lose focus when minimizing an app
345435 }
346436
347437 function test_windowMinimizeAll() {
348438 var apps = ["unity8-dash", "dialer-app", "camera-app"];
349439 apps.forEach(startApplication);
350− verify(ApplicationManager.count == 3);
440+ verify(topSurfaceList.count == 3);
351441 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
352442 tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
353443 }
354444
355445 function test_windowClose() {
356− var apps = ["unity8-dash", "dialer-app", "camera-app"];
357− apps.forEach(startApplication);
358− verify(ApplicationManager.count == 3);
359− var appName = "dialer-app";
360− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
361− verify(appDelegate);
362− ApplicationManager.focusApplication(appName);
446+ var dialerSurfaceId = topSurfaceList.nextId;
447+ var dialerDelegate = startApplication("dialer-app");
448+ verify(topSurfaceList.indexForId(dialerSurfaceId) !== -1);
449+ startApplication("gmail-webapp");
450+ verify(topSurfaceList.count == 3);
451+
452+ mouseClick(dialerDelegate);
453+
454+ var desktopStagePriv = findInvisibleChild(desktopStage, "DesktopStagePrivate");
455+ tryCompare(desktopStagePriv, "focusedAppDelegate", dialerDelegate);
456+
363457 keyClick(Qt.Key_F4, Qt.AltModifier); // Alt+F4 shortcut to close
364− verify(ApplicationManager.count == 2); // verify the app is gone
365− verify(ApplicationManager.findApplication(appName) === null); // and it's not in running apps
458+
459+ // verify the surface is gone
460+ tryCompare(topSurfaceList, "count", 2);
461+ verify(topSurfaceList.indexForId(dialerSurfaceId) === -1);
366462 }
367463
368464 function test_smashCursorKeys() {
369− var apps = ["unity8-dash", "dialer-app", "camera-app"];
465+ var apps = ["dialer-app", "gmail-webapp"];
370466 apps.forEach(startApplication);
371− verify(ApplicationManager.count == 3);
467+ verify(topSurfaceList.count == 3);
372468 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
373− tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
469+ tryCompare(MirFocusController, "focusedSurface", null); // verify no surface is focused
374470
375471 // now try pressing all 4 arrow keys + ctrl + meta
376472 keyClick(Qt.Key_Up | Qt.Key_Down | Qt.Key_Left | Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // smash it!!!
377− tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify still no app is focused
473+ tryCompare(MirFocusController, "focusedSurface", null); // verify still no surface is focused
378474 }
379475
380476 function test_minimizeApplicationHidesSurface() {
381− var dashApp = startApplication("unity8-dash");
477+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
478+ var dashSurface = topSurfaceList.surfaceAt(0);
479+ var dashSurfaceId = topSurfaceList.idAt(0);
382480
383− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
384− verify(dashDelegate);
481+ var decoratedWindow = findDecoratedWindow(dashSurfaceId);
482+ verify(decoratedWindow);
385483
386− findChild(dashDelegate, "decoratedWindow").minimize();
387− tryCompare(dashApp.session.lastSurface, "visible", false);
484+ tryCompare(dashSurface, "visible", true);
485+ decoratedWindow.minimize();
486+ tryCompare(dashSurface, "visible", false);
388487 }
389488
390489 function test_maximizeApplicationHidesSurfacesBehindIt() {
391− var dashApp = startApplication("unity8-dash");
392− var dialerApp = startApplication("dialer-app");
393− var gmailApp = startApplication("gmail-webapp");
490+ var dashDelegate = startApplication("unity8-dash");
491+ var dialerDelegate = startApplication("dialer-app");
492+ var gmailDelegate = startApplication("gmail-webapp");
394493
395− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
396− verify(dashDelegate);
397− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
398− verify(dialerDelegate);
399− var gmailDelegate = findChild(desktopStage, "appDelegate_gmail-webapp");
400− verify(gmailDelegate);
401−
402− // maximize
494+ // maximize without raising
403495 dialerDelegate.maximize();
404496 tryCompare(dialerDelegate, "visuallyMaximized", true);
405497
406− tryCompare(dashApp.session.lastSurface, "visible", false);
407− compare(gmailApp.session.lastSurface.visible, true);
498+ tryCompare(dashDelegate.surface, "visible", false);
499+ compare(gmailDelegate.surface.visible, true);
408500
409− // restore
501+ // restore without raising
410502 dialerDelegate.restoreFromMaximized();
411− compare(dashApp.session.lastSurface.visible, true);
412− compare(gmailApp.session.lastSurface.visible, true);
503+ compare(dashDelegate.surface.visible, true);
504+ compare(gmailDelegate.surface.visible, true);
413505 }
414506
415507 function test_applicationsBecomeVisibleWhenOccludingAppRemoved() {
416− var dashApp = startApplication("unity8-dash");
417− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
418− verify(dashDelegate);
508+ var dashApp = topSurfaceList.applicationAt(0);
419509
420− var dialerApp = startApplication("dialer-app");
421− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
510+ var dialerSurfaceId = topSurfaceList.nextId;
511+ var dialerDelegate = startApplication("dialer-app");
422512 verify(dialerDelegate);
513+ var dialerApp = dialerDelegate.application;
423514
424515 var dialerMaximizeButton = findChild(dialerDelegate, "maximizeWindowButton");
425516 verify(dialerMaximizeButton);
426517 mouseClick(dialerMaximizeButton);
427518
428− var mapApp = startApplication("map");
429− var mapDelegate = findChild(desktopStage, "appDelegate_map");
519+ var mapSurfaceId = topSurfaceList.nextId;
520+ var mapDelegate = startApplication("map");
430521 verify(mapDelegate);
522+ var mapApp = mapDelegate.application;
431523
432− var gmailApp = startApplication("gmail-webapp");
433− var gmailDelegate = findChild(desktopStage, "appDelegate_gmail-webapp");
524+ var gmailSurfaceId = topSurfaceList.nextId;
525+ var gmailDelegate = startApplication("gmail-webapp");
434526 verify(gmailDelegate);
435527
436528 var gmailMaximizeButton = findChild(gmailDelegate, "maximizeWindowButton");
@@ -440,29 +532,27 @@ Item {
440532 tryCompare(dialerDelegate, "visuallyMaximized", true);
441533 tryCompare(gmailDelegate, "visuallyMaximized", true);
442534
443− tryCompare(dashApp.session.lastSurface, "visible", false);
444− tryCompare(dialerApp.session.lastSurface, "visible", false);
445− tryCompare(mapApp.session.lastSurface, "visible", false);
535+ tryCompare(dashApp.surfaceList.get(0), "visible", false);
536+ tryCompare(dialerApp.surfaceList.get(0), "visible", false);
537+ tryCompare(mapApp.surfaceList.get(0), "visible", false);
446538
447539 ApplicationManager.stopApplication("gmail-webapp");
448540
449− compare(mapApp.session.lastSurface.visible, true);
450− tryCompare(dialerApp.session.lastSurface, "visible", true);
451− tryCompare(dashApp.session.lastSurface, "visible", false); // still occluded by maximised dialer
541+ tryCompare(mapApp.surfaceList.get(0), "visible", true);
542+ tryCompare(dialerApp.surfaceList.get(0), "visible", true);
543+ tryCompare(dashApp.surfaceList.get(0), "visible", false); // still occluded by maximised dialer
452544 }
453545
454546 function test_maximisedAppStaysVisibleWhenAppStarts() {
455− var dashApp = startApplication("unity8-dash");
456− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
457− verify(dashDelegate);
547+ var dashDelegate = startApplication("unity8-dash");
548+
458549 // maximize
459550 var dashMaximizeButton = findChild(dashDelegate, "maximizeWindowButton");
460551 verify(dashMaximizeButton);
461552 mouseClick(dashMaximizeButton);
462553 tryCompare(dashDelegate, "visuallyMaximized", true);
463554
464− var dialerApp = startApplication("dialer-app");
465− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
555+ var dialerDelegate = startApplication("dialer-app");
466556 verify(dialerDelegate);
467557
468558 compare(dialerDelegate.visible, true, "Dialer should be visible");
@@ -470,14 +560,11 @@ Item {
470560 }
471561
472562 function test_occlusionWithMultipleMaximized() {
473− var dashApp = startApplication("unity8-dash");
474− var dashAppDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
563+ var dashAppDelegate = startApplication("unity8-dash");
475564
476− var dialerApp = startApplication("dialer-app");
477− var dialerAppDelegate = findChild(desktopStage, "appDelegate_dialer-app");
565+ var dialerAppDelegate = startApplication("dialer-app");
478566
479− var facebookApp = startApplication("facebook-webapp");
480− var facebookAppDelegate = findChild(desktopStage, "appDelegate_facebook-webapp");
567+ var facebookAppDelegate = startApplication("facebook-webapp");
481568
482569 // all of them are in restored state now. all should be visible
483570 tryCompare(dashAppDelegate, "visible", true);
@@ -491,13 +578,13 @@ Item {
491578 tryCompare(facebookAppDelegate, "visible", true);
492579
493580 // Bring dash to front. make sure dash and the maximized facebook are visible, the restored one behind is hidden
494− ApplicationManager.focusApplication("unity8-dash");
581+ dashAppDelegate.focus = true;
495582 tryCompare(dashAppDelegate, "visible", true);
496583 tryCompare(dialerAppDelegate, "visible", false);
497584 tryCompare(facebookAppDelegate, "visible", true);
498585
499586 // Now focus the dialer app. all 3 should be visible again
500− ApplicationManager.focusApplication("dialer-app");
587+ dialerAppDelegate.focus = true;
501588 tryCompare(dashAppDelegate, "visible", true);
502589 tryCompare(dialerAppDelegate, "visible", true);
503590 tryCompare(facebookAppDelegate, "visible", true);
@@ -510,20 +597,15 @@ Item {
510597 }
511598
512599 function test_dropShadow() {
513− // verify the drop shadow is not visible initially
514− verify(PanelState.dropShadow == false);
515−
516600 // start an app, maximize it
517− var facebookApp = startApplication("facebook-webapp");
518− var facebookAppDelegate = findChild(desktopStage, "appDelegate_facebook-webapp");
601+ var facebookAppDelegate = startApplication("facebook-webapp");
519602 facebookAppDelegate.maximize();
520603
521604 // verify the drop shadow is still not visible
522605 verify(PanelState.dropShadow == false);
523606
524607 // start a foreground app, not maximized
525− var dialerApp = startApplication("dialer-app");
526− var dialerAppDelegate = findChild(desktopStage, "appDelegate_dialer-app");
608+ var dialerAppDelegate = startApplication("dialer-app");
527609
528610 // verify the drop shadow becomes visible
529611 verify(PanelState.dropShadow == true);
@@ -532,7 +614,7 @@ Item {
532614 ApplicationManager.stopApplication("facebook-webapp");
533615
534616 // verify the drop shadow is gone
535− verify(PanelState.dropShadow == false);
617+ tryCompare(PanelState, "dropShadow", false);
536618 }
537619 }
538620}
tests/qmltests/Stages/tst_PhoneStage.qml+272−82
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -22,8 +22,10 @@ import "../../../qml/Components"
2222import "../../../qml/Stages"
2323import Ubuntu.Components 1.3
2424import Unity.Application 0.1
25+import WindowManager 0.1
2526
2627Item {
28+ id: root
2729 width: units.gu(70)
2830 height: units.gu(70)
2931
@@ -38,63 +40,40 @@ Item {
3840 interactive: true
3941 shellOrientation: Qt.PortraitOrientation
4042 orientations: Orientations {}
43+ applicationManager: ApplicationManager
44+ topLevelSurfaceList: TopLevelSurfaceList {
45+ id: topLevelSurfaceList
46+ applicationsModel: ApplicationManager
47+ }
4148 }
4249
43− Rectangle {
44− anchors { fill: parent; leftMargin: phoneStage.width }
45−
46− Column {
47− id: buttons
48− anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
49− spacing: units.gu(1)
50− Button {
51− anchors { left: parent.left; right: parent.right }
52− text: "Add App"
53− activeFocusOnPress: false
54− onClicked: {
55− testCase.addApps();
56− }
57− }
58− Button {
59− anchors { left: parent.left; right: parent.right }
60− text: "Remove Selected"
61− activeFocusOnPress: false
62− onClicked: {
63− ApplicationManager.stopApplication(ApplicationManager.get(appList.selectedAppIndex).appId);
64− }
65− }
66− Button {
67− anchors { left: parent.left; right: parent.right }
68− text: "Stop Selected"
69− activeFocusOnPress: false
70− onClicked: {
71− ApplicationManager.get(appList.selectedAppIndex).setState(ApplicationInfoInterface.Stopped);
50+ Flickable {
51+ contentHeight: controlRect.height
52+
53+ anchors.top: root.top
54+ anchors.bottom: root.bottom
55+ anchors.right: root.right
56+ width: units.gu(30)
57+ Rectangle {
58+ id: controlRect
59+ anchors { left: parent.left; right: parent.right }
60+ height: childrenRect.height + units.gu(2)
61+ color: "darkGrey"
62+ Column {
63+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
64+ spacing: units.gu(1)
65+ EdgeBarrierControls {
66+ id: edgeBarrierControls
67+ text: "Drag here to pull out spread"
68+ backgroundColor: "blue"
69+ onDragged: { phoneStage.pushRightEdge(amount); }
70+ Component.onCompleted: {
71+ edgeBarrierControls.target = testCase.findChild(phoneStage, "edgeBarrierController");
72+ }
7273 }
73− }
74− EdgeBarrierControls {
75− id: edgeBarrierControls
76− text: "Drag here to pull out spread"
77− backgroundColor: "blue"
78− onDragged: { phoneStage.pushRightEdge(amount); }
79− Component.onCompleted: {
80− edgeBarrierControls.target = testCase.findChild(phoneStage, "edgeBarrierController");
81− }
82− }
83− }
84− ListView {
85− id: appList
86− property int selectedAppIndex
87− anchors { left: parent.left; right: parent.right; top: buttons.bottom; bottom: parent.bottom }
88− boundsBehavior: Flickable.StopAtBounds
89− model: ApplicationManager
90− delegate: Rectangle {
91− anchors { left: parent.left; right: parent.right }
92− height: units.gu(2)
93− color: appList.selectedAppIndex === model.index ? "red" : "white"
94− Text { anchors.fill: parent; text: model.appId }
95− MouseArea {
96− anchors.fill: parent
97− onPressed: { appList.selectedAppIndex = model.index; }
74+ Repeater {
75+ model: ApplicationManager.availableApplications
76+ ApplicationCheckBox { appId: modelData }
9877 }
9978 }
10079 }
@@ -105,18 +84,41 @@ Item {
10584 name: "PhoneStage"
10685 when: windowShown
10786
87+ function findAppWindowForSurfaceId(surfaceId) {
88+ var delegateObjectName = "spreadDelegate_" + surfaceId;
89+ var spreadDelegate = findChild(phoneStage, delegateObjectName);
90+ if (!spreadDelegate) {
91+ console.warn("Failed to find " + delegateObjectName + " in phoneStage ("+phoneStage+")");
92+ return null;
93+ }
94+ var appWindow = findChild(spreadDelegate, "appWindow");
95+ return appWindow;
96+ }
97+
98+ // Waits until ApplicationWindow has moved from showing a splash screen to displaying
99+ // the application surface.
100+ function waitUntilAppSurfaceShowsUp(surfaceId) {
101+ var appWindow = findAppWindowForSurfaceId(surfaceId);
102+ verify(appWindow);
103+ var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
104+ verify(appWindowStates);
105+ tryCompare(appWindowStates, "state", "surface");
106+ }
107+
108108 function addApps(count) {
109109 if (count == undefined) count = 1;
110110 for (var i = 0; i < count; i++) {
111+ var appSurfaceId = topLevelSurfaceList.nextId;
111112 var app = ApplicationManager.startApplication(ApplicationManager.availableApplications[ApplicationManager.count])
112113 tryCompare(app, "state", ApplicationInfoInterface.Running)
113114 var spreadView = findChild(phoneStage, "spreadView");
114115 tryCompare(spreadView, "contentX", -spreadView.shift);
116+ waitUntilAppSurfaceShowsUp(appSurfaceId);
115117 waitForRendering(phoneStage)
116118 }
117119 }
118120
119− function goToSpread() {
121+ function performEdgeSwipeToShowAppSpread() {
120122 var spreadView = findChild(phoneStage, "spreadView");
121123
122124 // Keep it inside the PhoneStage otherwise the controls on the right side will
@@ -130,9 +132,46 @@ Item {
130132 true /* beginTouch */, true /* endTouch */, units.gu(10), 50);
131133
132134 tryCompare(spreadView, "phase", 2);
135+ tryCompare(spreadView, "flicking", false);
136+ tryCompare(spreadView, "moving", false);
133137 waitForRendering(phoneStage);
134138 }
135139
140+ function swipeSurfaceUpwards(surfaceId) {
141+ var appWindow = findAppWindowForSurfaceId(surfaceId);
142+ verify(appWindow);
143+
144+ // Swipe from the left side of the surface as it's the one most likely
145+ // to not be covered by other surfaces when they're all being shown in the spread
146+ touchFlick(appWindow,
147+ appWindow.width * 0.1, appWindow.height / 2,
148+ appWindow.width * 0.1, -appWindow.height / 2);
149+ }
150+
151+ function switchToSurface(targetSurfaceId) {
152+ performEdgeSwipeToShowAppSpread();
153+
154+ waitUntilAppDelegateStopsMoving(targetSurfaceId);
155+
156+ // TODO: won't work if there are many items in the spread. in this case
157+ // you might have to drag the list to the right or left a bit to better
158+ // expose the target surface. Improve this code if needed.
159+ var targetAppWindow = findAppWindowForSurfaceId(targetSurfaceId);
160+ verify(targetAppWindow);
161+ tap(targetAppWindow, 10, 10);
162+ }
163+
164+ function waitUntilAppDelegateStopsMoving(targetSurfaceId)
165+ {
166+ var targetAppDelegate = findChild(phoneStage, "spreadDelegate_" + targetSurfaceId);
167+ verify(targetAppDelegate);
168+ var lastValue = undefined;
169+ do {
170+ lastValue = targetAppDelegate.animatedProgress;
171+ wait(300);
172+ } while (lastValue != targetAppDelegate.animatedProgress);
173+ }
174+
136175 function test_shortFlick() {
137176 addApps(2)
138177 var startX = phoneStage.width - units.gu(1);
@@ -200,7 +239,7 @@ Item {
200239 tryCompare(spreadView, "phase", data.targetPhase)
201240
202241 if (data.targetPhase == 2) {
203− var app2 = findChild(spreadView, "appDelegate2");
242+ var app2 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(2));
204243 tryCompare(app2, "swipeToCloseEnabled", true);
205244 mouseClick(app2, units.gu(1), units.gu(1));
206245 }
@@ -226,11 +265,11 @@ Item {
226265
227266 var spreadView = findChild(phoneStage, "spreadView");
228267
229− goToSpread();
268+ performEdgeSwipeToShowAppSpread();
230269
231270 tryCompare(spreadView, "phase", 2);
232271
233− var tile = findChild(spreadView, "appDelegate" + data.index);
272+ var tile = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(data.index));
234273 var appId = ApplicationManager.get(data.index).appId;
235274
236275 if (tile.mapToItem(spreadView).x > spreadView.width) {
@@ -266,7 +305,7 @@ Item {
266305 var spreadView = findChild(phoneStage, "spreadView");
267306 var selectedApp = ApplicationManager.get(data.index);
268307
269− goToSpread();
308+ performEdgeSwipeToShowAppSpread();
270309
271310 phoneStage.select(selectedApp.appId);
272311
@@ -280,7 +319,7 @@ Item {
280319
281320 var focusedAppId = ApplicationManager.focusedApplicationId;
282321
283− goToSpread();
322+ performEdgeSwipeToShowAppSpread();
284323
285324 mouseClick(phoneStage, units.gu(1), units.gu(1));
286325
@@ -292,15 +331,26 @@ Item {
292331 compare(focusedAppId, ApplicationManager.focusedApplicationId);
293332 }
294333
334+ function init() {
335+ // wait until unity8-dash is up and running.
336+ // it's started automatically by ApplicationManager mock implementation
337+ tryCompare(ApplicationManager, "count", 1);
338+ var dashApp = ApplicationManager.findApplication("unity8-dash");
339+ verify(dashApp);
340+ tryCompare(dashApp, "state", ApplicationInfoInterface.Running);
341+ }
342+
295343 function cleanup() {
296− while (ApplicationManager.count > 1) {
297− var oldCount = ApplicationManager.count;
298− var closingIndex = ApplicationManager.focusedApplicationId == "unity8-dash" ? 1 : 0
299− ApplicationManager.stopApplication(ApplicationManager.get(closingIndex).appId)
300− tryCompare(ApplicationManager, "count", oldCount - 1)
301− }
344+ killApps();
345+
302346 phoneStage.shellOrientationAngle = 0;
303347 phoneStage.select(ApplicationManager.get(0).appId);
348+
349+ // wait for PhoneStage to stabilize back into its initial state
350+ var spreadView = findChild(phoneStage, "spreadView");
351+ while (spreadView.phase !== 0 || spreadView.contentX !== -spreadView.shift) {
352+ wait(50);
353+ }
304354 }
305355
306356 function test_focusNewTopMostAppAfterFocusedOneClosesItself() {
@@ -323,16 +373,16 @@ Item {
323373 function test_cantCloseWhileSnapping() {
324374 addApps(2);
325375
326− goToSpread();
376+ performEdgeSwipeToShowAppSpread();
327377
328378 var spreadView = findChild(phoneStage, "spreadView");
329379 var selectedApp = ApplicationManager.get(2);
330380
331− goToSpread();
381+ performEdgeSwipeToShowAppSpread();
332382
333− var app0 = findChild(spreadView, "appDelegate0");
334− var app1 = findChild(spreadView, "appDelegate1");
335− var app2 = findChild(spreadView, "appDelegate2");
383+ var app0 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
384+ var app1 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
385+ var app2 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(2));
336386
337387 var dragArea0 = findChild(app0, "dragArea");
338388 var dragArea1 = findChild(app1, "dragArea");
@@ -382,10 +432,10 @@ Item {
382432 addApps(2);
383433
384434 if (data.inSpread) {
385− goToSpread();
435+ performEdgeSwipeToShowAppSpread();
386436 }
387437
388− var focusedDelegate = findChild(phoneStage, "appDelegate0");
438+ var focusedDelegate = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
389439 phoneStage.inverseProgress = data.leftEdgeDragWidth;
390440
391441 tryCompare(focusedDelegate, "x", data.shouldMoveApp ? data.leftEdgeDragWidth : 0);
@@ -398,9 +448,9 @@ Item {
398448 function test_focusedAppIsTheOnlyRunningApp() {
399449 addApps(2);
400450
401− var delegateA = findChild(phoneStage, "appDelegate0");
451+ var delegateA = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
402452 verify(delegateA);
403− var delegateB = findChild(phoneStage, "appDelegate1");
453+ var delegateB = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
404454 verify(delegateB);
405455
406456 // A is focused and running, B is unfocused and suspended
@@ -410,7 +460,7 @@ Item {
410460 compare(delegateB.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
411461
412462 // Switch foreground/focused appp from A to B
413− goToSpread();
463+ performEdgeSwipeToShowAppSpread();
414464 phoneStage.select(delegateB.application.appId);
415465
416466 // Now it's the other way round
@@ -424,14 +474,14 @@ Item {
424474 function test_dashRemainsRunningIfStageIsToldSo() {
425475 addApps(1);
426476
427− var delegateDash = findChild(phoneStage, "appDelegate1");
477+ var delegateDash = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
428478 verify(delegateDash);
429479 compare(delegateDash.application.appId, "unity8-dash");
430480
431− var delegateOther = findChild(phoneStage, "appDelegate0");
481+ var delegateOther = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
432482 verify(delegateOther);
433483
434− goToSpread();
484+ performEdgeSwipeToShowAppSpread();
435485 phoneStage.select("unity8-dash");
436486
437487 tryCompare(delegateDash, "focus", true);
@@ -439,7 +489,7 @@ Item {
439489 compare(delegateOther.focus, false);
440490 compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
441491
442− goToSpread();
492+ performEdgeSwipeToShowAppSpread();
443493 phoneStage.select(delegateOther.application.appId);
444494
445495 // The other app gets focused and running but dash is kept running despite being unfocused
@@ -452,7 +502,7 @@ Item {
452502 function test_foregroundAppIsSuspendedWhenStageIsSuspended() {
453503 addApps(1);
454504
455− var delegate = findChild(phoneStage, "appDelegate0");
505+ var delegate = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
456506 verify(delegate);
457507
458508 compare(delegate.focus, true);
@@ -476,5 +526,145 @@ Item {
476526 }
477527 tryCompare(spreadView, "phase", 2);
478528 }
529+
530+ function test_closeSurfaceOfMultiSurfaceApp() {
531+ var surface1Id = topLevelSurfaceList.nextId;
532+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
533+ waitUntilAppSurfaceShowsUp(surface1Id);
534+
535+ var surface2Id = topLevelSurfaceList.nextId;
536+ verify(surface1Id !== surface2Id); // sanity checking
537+ webbrowserApp.createSurface();
538+ waitUntilAppSurfaceShowsUp(surface2Id);
539+
540+ performEdgeSwipeToShowAppSpread();
541+
542+ var appDelegate = findChild(phoneStage, "spreadDelegate_" + surface1Id);
543+ verify(appDelegate);
544+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
545+
546+ compare(webbrowserApp.surfaceList.count, 2);
547+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
548+
549+ swipeSurfaceUpwards(surface1Id);
550+
551+ // Surface must eventually be gone
552+ tryCompareFunction(function() { return topLevelSurfaceList.indexForId(surface1Id); }, -1);
553+ tryCompare(webbrowserApp.surfaceList, "count", 1);
554+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
555+ }
556+
557+ /*
558+ 1- Suspended app gets killed behind the scenes, causing its surface to go zombie.
559+ 2- Surface gets screenshotted and removed. Its slot in the topLevelSurfaceList remains,
560+ though (so ApplicationWindow can display the screenshot in its place).
561+ 3- User taps on the screenshot of the long-gone surface.
562+
563+ Expected outcome:
564+ Application gets relaunched. Its new surface will seamlessly replace the screenshot.
565+ */
566+ function test_selectSuspendedAppWithoutSurface() {
567+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
568+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
569+ var dashSurface = topLevelSurfaceList.surfaceAt(0);
570+
571+ var webbrowserSurfaceId = topLevelSurfaceList.nextId;
572+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
573+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
574+
575+ switchToSurface(dashSurfaceId);
576+
577+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
578+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
579+
580+ compare(webbrowserApp.surfaceList.count, 1);
581+
582+ // simulate the suspended app being killed by the out-of-memory daemon
583+ webbrowserApp.surfaceList.get(0).setLive(false);
584+
585+ // wait until the surface is gone
586+ tryCompare(webbrowserApp.surfaceList, "count", 0);
587+
588+ compare(topLevelSurfaceList.surfaceAt(topLevelSurfaceList.indexForId(webbrowserSurfaceId)), null);
589+
590+ switchToSurface(webbrowserSurfaceId);
591+
592+ // webbrowser should have been brought to front
593+ tryCompareFunction(function(){return topLevelSurfaceList.idAt(0);}, webbrowserSurfaceId);
594+
595+ // and it should eventually get a new surface and get resumed
596+ tryCompareFunction(function(){return topLevelSurfaceList.surfaceAt(0) !== null;}, true);
597+ compare(topLevelSurfaceList.count, 2); // still two top-level items
598+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
599+ compare(webbrowserApp.surfaceList.count, 1);
600+ }
601+
602+ /*
603+ 1 - user suspends an application (ie, focus a surface from a different application)
604+ 2 - That suspended application gets killed, causing its surface to go zombie
605+ 3 - user goes to spread and closes that zombie surface. Actually, by that time
606+ Tablet shell will be displaying a screenshot of it instead, so there's no
607+ MirSurface whatsoever backing it up.
608+ */
609+ function test_closeZombieSurface()
610+ {
611+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
612+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
613+ var dashSurface = topLevelSurfaceList.surfaceAt(0);
614+
615+ var webbrowserSurfaceId = topLevelSurfaceList.nextId;
616+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
617+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
618+
619+ switchToSurface(dashSurfaceId);
620+
621+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
622+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
623+
624+ compare(webbrowserApp.surfaceList.count, 1);
625+
626+ // simulate the suspended app being killed by the out-of-memory daemon
627+ webbrowserApp.surfaceList.get(0).setLive(false);
628+
629+ // wait until the surface is gone
630+ tryCompare(webbrowserApp.surfaceList, "count", 0);
631+ compare(topLevelSurfaceList.surfaceAt(topLevelSurfaceList.indexForId(webbrowserSurfaceId)), null);
632+
633+ performEdgeSwipeToShowAppSpread();
634+
635+ {
636+ var appDelegate = findChild(phoneStage, "spreadDelegate_" + webbrowserSurfaceId);
637+ verify(appDelegate);
638+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
639+ }
640+
641+ swipeSurfaceUpwards(webbrowserSurfaceId);
642+
643+ // webbrowser entry is nowhere to be seen
644+ tryCompareFunction(function(){return topLevelSurfaceList.indexForId(webbrowserSurfaceId);}, -1);
645+
646+ // nor is its app
647+ compare(ApplicationManager.findApplication("webbrowser-app"), null);
648+
649+ // only unity8-dash surface is left
650+ compare(topLevelSurfaceList.count, 1);
651+ }
652+
653+
654+ /*
655+ Check that when an application starts while the spread is open, the
656+ spread closes and that new app is brought to front (gets focused).
657+ */
658+ function test_launchAppWithSpreadOpen()
659+ {
660+ performEdgeSwipeToShowAppSpread();
661+
662+ var webbrowserSurfaceId = topLevelSurfaceList.nextId;
663+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
664+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
665+
666+ compare(topLevelSurfaceList.idAt(0), webbrowserSurfaceId);
667+ compare(webbrowserApp.focused, true);
668+ }
479669 }
480670}
tests/qmltests/Stages/tst_SessionContainer.qmldeleted−331
@@ -1,331 +0,0 @@
1−/*
2− * Copyright 2014-2015 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import QtQuick.Layouts 1.1
19−import QtTest 1.0
20−import Unity.Test 0.1 as UT
21−import Ubuntu.Components 1.3
22−import Unity.Application 0.1
23−import "../../../qml/Stages"
24−
25−Rectangle {
26− color: "red"
27− id: root
28− width: units.gu(80)
29− height: units.gu(70)
30−
31− Connections {
32− target: sessionContainerLoader.status === Loader.Ready ? sessionContainerLoader.item : null
33− onSessionChanged: {
34− sessionCheckbox.checked = sessionContainerLoader.item.session !== null
35− }
36− }
37−
38− Component {
39− id: sessionContainerComponent
40−
41− SessionContainer {
42− id: sessionContainer
43− anchors.fill: parent
44− focus: true
45− interactive: true
46− }
47− }
48−
49− Loader {
50− id: sessionContainerLoader
51− focus: true
52− anchors {
53− top: parent.top
54− bottom: parent.bottom
55− left: parent.left
56− }
57− width: units.gu(40)
58− sourceComponent: sessionContainerComponent
59− }
60−
61− Rectangle {
62− color: "white"
63− anchors {
64− top: parent.top
65− bottom: parent.bottom
66− left: sessionContainerLoader.right
67− right: parent.right
68− }
69−
70− ColumnLayout {
71− anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
72− spacing: units.gu(1)
73−
74− RowLayout {
75− Layout.fillWidth: true
76−
77− CheckBox {
78− id: sessionCheckbox;
79− checked: false;
80− activeFocusOnPress: false
81− onCheckedChanged: {
82− if (sessionContainerLoader.status !== Loader.Ready)
83− return;
84−
85− if (checked) {
86− var fakeSession = SessionManager.createSession("music-player", Qt.resolvedUrl("../Dash/artwork/music-player-design.png"));
87− sessionContainerLoader.item.session = fakeSession;
88− fakeSession.createSurface();
89− } else {
90− sessionContainerLoader.item.session.release();
91− }
92− }
93− }
94−
95− Label {
96− text: "session"
97− anchors.verticalCenter: parent.verticalCenter
98− }
99− }
100−
101− Rectangle {
102− border {
103− color: "black"
104− width: 1
105− }
106− anchors {
107− left: parent.left
108− right: parent.right
109− }
110− Layout.preferredHeight: sessionChildrenControl.height
111−
112− RecursingChildSessionControl {
113− id: sessionChildrenControl
114− anchors { left: parent.left; right: parent.right; }
115−
116− session: sessionContainerLoader.item ? sessionContainerLoader.item.session : null
117− }
118− }
119− }
120− }
121−
122− SignalSpy {
123− id: sessionSpy
124− target: SessionManager
125− signalName: "sessionStopping"
126− }
127−
128− UT.UnityTestCase {
129− id: testCase
130− name: "SessionContainer"
131−
132− function init() {
133− // reload our test subject to get it in a fresh state once again
134− sessionContainerLoader.active = false;
135− sessionCheckbox.checked = false;
136− sessionContainerLoader.active = true;
137−
138− tryCompare(sessionContainerLoader.item, "session", null);
139− sessionSpy.clear();
140− }
141−
142− when: windowShown
143−
144− function test_addChildSession_data() {
145− return [ { tag: "count=1", count: 1 },
146− { tag: "count=4", count: 4 } ];
147− }
148−
149− function test_addChildSession(data) {
150− sessionCheckbox.checked = true;
151− var sessionContainer = sessionContainerLoader.item;
152− compare(sessionContainer.childSessions.count(), 0);
153−
154− var i;
155− var sessions = [];
156− for (i = 0; i < data.count; i++) {
157− var session = ApplicationTest.addChildSession(sessionContainer.session,
158− "gallery",
159− true);
160− session.createSurface();
161− sessionContainer.session.addChildSession(session);
162− compare(sessionContainer.childSessions.count(), i+1);
163−
164− sessions.push(session);
165− }
166−
167− for (i = data.count-1; i >= 0; i--) {
168− ApplicationTest.removeSession(sessions[i]);
169− tryCompareFunction(function() { return sessionContainer.childSessions.count(); }, i);
170− }
171− tryCompare(sessionSpy, "count", data.count);
172− }
173−
174− function test_childSessionDestructionReturnsFocusToSiblingOrParent() {
175− sessionCheckbox.checked = true;
176− var sessionContainer = sessionContainerLoader.item;
177− compare(sessionContainer.childSessions.count(), 0);
178−
179− var i;
180− var sessions = [];
181− // 3 sessions should cover all edge cases
182− for(i = 0; i < 3; i++) {
183− var a_session = ApplicationTest.addChildSession(
184− sessionContainer.session, "gallery", true
185− )
186−
187− a_session.createSurface();
188− sessionContainer.session.addChildSession(a_session);
189− compare(sessionContainer.childSessions.count(), i + 1);
190−
191− sessions.push(a_session);
192− }
193−
194− var a_session;
195− while(a_session = sessions.pop()) {
196− compare(a_session.lastSurface.activeFocus, true);
197−
198− ApplicationTest.removeSurface(a_session.lastSurface);
199− ApplicationTest.removeSession(a_session);
200−
201− if (sessions.length > 0) {
202− // active focus should have gone to the yongest remaining sibling
203− var previousSiblingSurface = sessions[sessions.length - 1].lastSurface
204− tryCompare(previousSiblingSurface, "activeFocus", true);
205− } else {
206− // active focus should have gone to the parent surface
207− tryCompare(sessionContainer.session.lastSurface, "activeFocus", true);
208− }
209− }
210− }
211−
212− function test_nestedChildSessions_data() {
213− return [ { tag: "depth=2", depth: 2 },
214− { tag: "depth=8", depth: 8 }
215− ];
216− }
217−
218− function test_nestedChildSessions(data) {
219− sessionCheckbox.checked = true;
220− var sessionContainer = sessionContainerLoader.item;
221− compare(sessionContainer.childSessions.count(), 0);
222−
223− var i;
224− var sessions = [];
225− var lastSession = sessionContainer.session;
226− var delegate;
227− var container = sessionContainer;
228− for (i = 0; i < data.depth; i++) {
229− var session = ApplicationTest.addChildSession(lastSession,
230− "gallery",
231− true);
232− session.createSurface();
233− lastSession.addChildSession(session);
234− compare(container.childSessions.count(), 1);
235− sessions.push(session);
236−
237− delegate = findChild(container, "childDelegate0");
238− container = findChild(delegate, "sessionContainer");
239− lastSession = session;
240− }
241−
242− for (i = data.depth-1; i >= 0; i--) {
243− ApplicationTest.removeSession(sessions[i]);
244− }
245−
246− tryCompareFunction(function() { return sessionContainer.childSessions.count(); }, 0);
247− tryCompare(sessionSpy, "count", data.depth);
248− }
249−
250− function test_childrenAdjustForParentSize() {
251− sessionCheckbox.checked = true;
252− var sessionContainer = sessionContainerLoader.item;
253−
254− var session = ApplicationTest.addChildSession(sessionContainer.session,
255− "gallery",
256− true);
257− session.createSurface();
258− sessionContainer.session.addChildSession(session);
259−
260− var delegate = findChild(sessionContainer, "childDelegate0");
261− var childContainer = findChild(delegate, "sessionContainer");
262−
263− tryCompareFunction(function() { return childContainer.height === sessionContainer.height; }, true);
264− tryCompareFunction(function() { return childContainer.width === sessionContainer.width; }, true);
265− tryCompareFunction(function() { return childContainer.x === 0; }, true);
266− tryCompareFunction(function() { return childContainer.y === 0; }, true);
267−
268− sessionContainer.anchors.margins = units.gu(2);
269−
270− tryCompareFunction(function() { return childContainer.height === sessionContainer.height; }, true);
271− tryCompareFunction(function() { return childContainer.width === sessionContainer.width; }, true);
272− tryCompareFunction(function() { return childContainer.x === 0; }, true);
273− tryCompareFunction(function() { return childContainer.y === 0; }, true);
274− }
275−
276− function isContainerAnimating(container) {
277− var animation = findInvisibleChild(container, "sessionAnimation");
278− if (!animation) return false;
279−
280− var animating = false;
281− for (var i = 0; i < animation.transitions.length; ++i) {
282− if (animation.transitions[i].running) {
283− return true;
284− }
285− }
286− return false;
287− }
288−
289− function test_childrenAnimate() {
290− sessionCheckbox.checked = true;
291− var sessionContainer = sessionContainerLoader.item;
292−
293− var session = ApplicationTest.addChildSession(sessionContainer.session,
294− "gallery",
295− true);
296−
297− var delegate = findChild(sessionContainer, "childDelegate0");
298− var childContainer = findChild(delegate, "sessionContainer");
299−
300− // wait for animation to begin
301− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
302− // wait for animation to end
303− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
304−
305− ApplicationTest.removeSession(session);
306−
307− // wait for animation to begin
308− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
309− // wait for animation to end
310− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
311−
312− tryCompareFunction(function() { return sessionContainer.childSessions.count() }, 0);
313− }
314−
315− function test_childSessionsRemovedOnStopWhenNotLive() {
316− sessionCheckbox.checked = true;
317− var sessionContainer = sessionContainerLoader.item;
318−
319− var session = ApplicationTest.addChildSession(sessionContainer.session,
320− "gallery",
321− false);
322−
323− var delegate = findChild(sessionContainer, "childDelegate0");
324− var childContainer = findChild(delegate, "sessionContainer");
325−
326− ApplicationTest.removeSession(session);
327−
328− tryCompareFunction(function() { return sessionContainer.childSessions.count() }, 0);
329− }
330− }
331−}
tests/qmltests/Stages/tst_SpreadDelegate.qml+3−2
@@ -61,6 +61,7 @@ Rectangle {
6161 swipeToCloseEnabled: swipeToCloseCheckbox.checked
6262 closeable: closeableCheckbox.checked
6363 application: fakeApplication
64+ surface: fakeApplication && fakeApplication.surfaceList.count > 0 ? fakeApplication.surfaceList.get(0) : null
6465 shellOrientationAngle: shellOrientationAngleSelector.value
6566 shellOrientation: {
6667 switch (shellOrientationAngleSelector.value) {
@@ -162,8 +163,8 @@ Rectangle {
162163 unloadSpreadDelegate();
163164 spyClosedSignal.clear();
164165 shellOrientationAngleSelector.selectedIndex = 0;
165− ApplicationManager.stopApplication(root.fakeApplication.appId);
166166 root.fakeApplication = null;
167+ killApps();
167168 }
168169
169170 function restartWithApp(appId) {
@@ -258,7 +259,7 @@ Rectangle {
258259 function test_loadingLandscapeOnlyAppWhenShellInPortrait() {
259260 loadWithWeatherApp.clicked();
260261
261− var appWindow = findChild(spreadDelegate, "appWindow_ubuntu-weather-app");
262+ var appWindow = findChild(spreadDelegate, "appWindow");
262263 verify(appWindow);
263264
264265 // It must have landscape dimensions as it does not support portrait
tests/qmltests/Stages/tst_SurfaceContainer.qml+242−46
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -15,28 +15,42 @@
1515 */
1616
1717import QtQuick 2.4
18+import QtQuick.Layouts 1.1
1819import QtTest 1.0
19−import Unity.Test 0.1
20−import ".."
21−import "../../../qml/Stages"
20+import Unity.Test 0.1 as UT
2221import Ubuntu.Components 1.3
2322import Unity.Application 0.1
23+import "../../../qml/Stages"
2424
2525Rectangle {
26− color: "black"
26+ color: "red"
2727 id: root
28− width: units.gu(70)
28+ width: units.gu(80)
2929 height: units.gu(70)
3030
31+ Connections {
32+ target: surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
33+ onSurfaceChanged: {
34+ surfaceCheckbox.checked = surfaceContainerLoader.item.surface !== null
35+ }
36+ }
37+
3138 Component {
3239 id: surfaceContainerComponent
40+
3341 SurfaceContainer {
3442 anchors.fill: parent
43+ focus: true
3544 interactive: interactiveCheckbox.checked
45+ Component.onDestruction: {
46+ surfaceContainerLoader.itemDestroyed = true;
47+ }
3648 }
3749 }
50+
3851 Loader {
3952 id: surfaceContainerLoader
53+ focus: true
4054 anchors {
4155 top: parent.top
4256 topMargin: fullscreenCheckbox.checked ? 0 : units.gu(3)
@@ -44,6 +58,7 @@ Rectangle {
4458 left: parent.left
4559 }
4660 width: units.gu(40)
61+ property bool itemDestroyed: false
4762 sourceComponent: surfaceContainerComponent
4863 }
4964
@@ -56,80 +71,257 @@ Rectangle {
5671 right: parent.right
5772 }
5873
59− Column {
74+ ColumnLayout {
6075 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
6176 spacing: units.gu(1)
62− Row {
63− anchors { left: parent.left; right: parent.right }
77+
78+ RowLayout {
79+ Layout.fillWidth: true
80+
81+ Row {
82+ CheckBox {id: fullscreenCheckbox; checked: true; activeFocusOnPress: false }
83+ Label { text: "fullscreen" }
84+ }
85+ Row {
86+ CheckBox {id: interactiveCheckbox; checked: true; activeFocusOnPress: false }
87+ Label { text: "interactive" }
88+ }
89+ }
90+
91+ RowLayout {
92+ Layout.fillWidth: true
93+
6494 CheckBox {
6595 id: surfaceCheckbox;
6696 checked: false;
97+ activeFocusOnPress: false
6798 onCheckedChanged: {
6899 if (surfaceContainerLoader.status !== Loader.Ready)
69100 return;
70101
71102 if (checked) {
72− var fakeSurface = SurfaceManager.createSurface("fake-surface",
73− Mir.NormalType,
74− Mir.RestoredState,
75− Qt.resolvedUrl("../Dash/artwork/music-player-design.png"));
76− surfaceContainerLoader.item.surface = fakeSurface;
103+ var application = ApplicationManager.add("music-app");
104+ application.manualSurfaceCreation = true;
105+ application.setState(ApplicationInfoInterface.Starting);
106+
107+ surfaceContainerLoader.item.surface = SurfaceManager.createSurface("foo",
108+ Mir.NormalType, Mir.MaximizedState, application.screenshot);
109+
110+ application.setState(ApplicationInfoInterface.Running);
77111 } else {
78− surfaceContainerLoader.item.surface.setLive(false);
112+ if (surfaceContainerLoader.item.surface) {
113+ surfaceContainerLoader.item.surface.setLive(false);
114+ }
115+ ApplicationManager.stopApplication("music-app");
79116 }
80117 }
81118 }
82− Label { text: "surface" }
83− }
84− Row {
85− anchors { left: parent.left; right: parent.right }
86− CheckBox {id: fullscreenCheckbox; checked: true; }
87− Label { text: "fullscreen" }
119+
120+ Label {
121+ text: "surface"
122+ anchors.verticalCenter: parent.verticalCenter
123+ }
88124 }
89− Row {
90− anchors { left: parent.left; right: parent.right }
91− CheckBox {id: interactiveCheckbox; checked: true; }
92− Label { text: "interactive" }
125+
126+ Rectangle {
127+ border {
128+ color: "black"
129+ width: 1
130+ }
131+ anchors {
132+ left: parent.left
133+ right: parent.right
134+ }
135+ Layout.preferredHeight: surfaceChildrenControl.height
136+
137+ RecursingChildSessionControl {
138+ id: surfaceChildrenControl
139+ anchors { left: parent.left; right: parent.right; }
140+
141+ surface: surfaceContainerLoader.item ? surfaceContainerLoader.item.surface : null
142+ }
93143 }
94− MouseTouchEmulationCheckbox {}
95144 }
96145 }
97146
98− SignalSpy {
99− id: surfaceSpy
100− target: SurfaceManager
101− signalName: "surfaceDestroyed"
102− }
103−
104− UnityTestCase {
147+ UT.UnityTestCase {
105148 id: testCase
106149 name: "SurfaceContainer"
107− when: windowShown
108150
109− property var surfaceContainer:
110− surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
151+ property Item surfaceContainer: surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
111152
112153 function cleanup() {
113154 // reload our test subject to get it in a fresh state once again
155+
156+ surfaceContainerLoader.itemDestroyed = false;
114157 surfaceContainerLoader.active = false;
158+
159+ tryCompare(surfaceContainerLoader, "status", Loader.Null);
160+ tryCompare(surfaceContainerLoader, "item", null);
161+ tryCompare(surfaceContainerLoader, "itemDestroyed", true);
162+
163+ killApps();
164+
115165 surfaceCheckbox.checked = false;
166+ interactiveCheckbox.checked = true;
167+
116168 surfaceContainerLoader.active = true;
169+ tryCompare(surfaceContainerLoader, "status", Loader.Ready);
170+ }
117171
118− tryCompare(surfaceContainerLoader.item, "surface", null);
119− surfaceSpy.clear();
172+ when: windowShown
120173
121− interactiveCheckbox.checked = true;
174+ function test_addChildSession_data() {
175+ return [ { tag: "count=1", count: 1 },
176+ { tag: "count=4", count: 4 } ];
122177 }
123178
124− function test_animateRemoval() {
179+ function test_addChildSession(data) {
125180 surfaceCheckbox.checked = true;
181+ var childSurfaces = testCase.findChild(surfaceContainer, "childSurfacesRepeater");
182+ compare(childSurfaces.count, 0);
126183
127− verify(surfaceContainer.surface !== null);
184+ var i;
185+ for (i = 0; i < data.count; i++) {
186+ surfaceContainer.surface.createPromptSurface();
187+ compare(childSurfaces.count, i+1);
188+ }
189+
190+ for (i = data.count-1; i >= 0; i--) {
191+ {
192+ var childPromptSurface = surfaceContainer.surface.promptSurfaceList.get(i);
193+ childPromptSurface.close();
194+ }
195+ tryCompareFunction(function() { return childSurfaces.count; }, i);
196+ }
197+ }
198+
199+ function test_childSessionDestructionReturnsFocusToSiblingOrParent() {
200+ surfaceCheckbox.checked = true;
201+ var childSurfaces = testCase.findChild(surfaceContainer, "childSurfacesRepeater");
202+ compare(childSurfaces.count, 0);
203+
204+ var i;
205+ // 3 surfaces should cover all edge cases
206+ for (i = 0; i < 3; i++) {
207+ surfaceContainer.surface.createPromptSurface();
208+ compare(childSurfaces.count, i+1);
209+ }
128210
129− surfaceContainer.surface.setLive(false);
211+ for (i = 2; i >= 0; --i) {
212+ var childPromptSurface = surfaceContainer.surface.promptSurfaceList.get(i);
213+ compare(childPromptSurface.activeFocus, true);
214+
215+ childPromptSurface.close();
216+ tryCompareFunction(function() { return childSurfaces.count; }, i);
217+
218+ if (i > 0) {
219+ // active focus should have gone to the yongest remaining sibling
220+ var previousSiblingSurface = surfaceContainer.surface.promptSurfaceList.get(i-1);
221+ tryCompare(previousSiblingSurface, "activeFocus", true);
222+ } else {
223+ // active focus should have gone to the parent surface
224+ tryCompare(surfaceContainer.surface, "activeFocus", true);
225+ }
226+ }
227+ }
130228
131− compare(surfaceContainer.state, "zombie");
132− tryCompare(surfaceContainer, "surface", null);
229+ function test_nestedChildSessions_data() {
230+ return [ { tag: "depth=2", depth: 2 },
231+ { tag: "depth=8", depth: 8 }
232+ ];
233+ }
234+ function test_nestedChildSessions(data) {
235+ surfaceCheckbox.checked = true;
236+
237+ var i;
238+ var container = surfaceContainer;
239+ var surface = container.surface;
240+ var surfaces = [surface];
241+ var parent_childSurfaces = [null];
242+ for (i = 0; i < data.depth; i++) {
243+ surface.createPromptSurface();
244+ var childSurfaces = testCase.findChild(container, "childSurfacesRepeater");
245+ compare(childSurfaces.count, 1);
246+
247+ var childDelegate = childSurfaces.itemAt(0);
248+ container = findChild(childDelegate, "surfaceContainer");
249+ {
250+ var animationsLoader = findChild(container, "animationsLoader");
251+ tryCompare(animationsLoader, "status", Loader.Ready);
252+ waitUntilTransitionsEnd(animationsLoader.item);
253+ }
254+ surface = container.surface;
255+
256+ surfaces.push(surface);
257+ parent_childSurfaces.push(childSurfaces);
258+ }
259+
260+ for (i = surfaces.length-1; i >= 0; i--) {
261+ surfaces[i].close();
262+ if (parent_childSurfaces[i]) {
263+ tryCompareFunction(function() { return parent_childSurfaces[i].count; }, 0);
264+ }
265+ }
266+ }
267+
268+ function test_childrenAdjustForParentSize() {
269+ surfaceCheckbox.checked = true;
270+
271+ surfaceContainer.surface.createPromptSurface();
272+
273+ var delegate = findChild(surfaceContainer, "childDelegate0");
274+ var childContainer = findChild(delegate, "surfaceContainer");
275+
276+ tryCompareFunction(function() { return childContainer.height === surfaceContainer.height; }, true);
277+ tryCompareFunction(function() { return childContainer.width === surfaceContainer.width; }, true);
278+ tryCompareFunction(function() { return childContainer.x === 0; }, true);
279+ tryCompareFunction(function() { return childContainer.y === 0; }, true);
280+
281+ surfaceContainer.anchors.margins = units.gu(2);
282+
283+ tryCompareFunction(function() { return childContainer.height === surfaceContainer.height; }, true);
284+ tryCompareFunction(function() { return childContainer.width === surfaceContainer.width; }, true);
285+ tryCompareFunction(function() { return childContainer.x === 0; }, true);
286+ tryCompareFunction(function() { return childContainer.y === 0; }, true);
287+ }
288+
289+ function isContainerAnimating(container) {
290+ var animationsLoader = findChild(container, "animationsLoader");
291+ if (!animationsLoader || animationsLoader.status != Loader.Ready) {
292+ return false;
293+ }
294+
295+ var animation = animationsLoader.item;
296+
297+ var animating = false;
298+ for (var i = 0; i < animation.transitions.length; ++i) {
299+ if (animation.transitions[i].running) {
300+ return true;
301+ }
302+ }
303+ return false;
304+ }
305+
306+ function test_childrenAnimate() {
307+ surfaceCheckbox.checked = true;
308+
309+ surfaceContainer.surface.createPromptSurface();
310+
311+ var delegate = findChild(surfaceContainer, "childDelegate0");
312+ var childContainer = findChild(delegate, "surfaceContainer");
313+
314+ // wait for animation to begin
315+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
316+ // wait for animation to end
317+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
318+
319+ surfaceContainer.surface.promptSurfaceList.get(0).close();
320+
321+ // wait for animation to begin
322+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
323+ // wait for animation to end
324+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
133325 }
134326
135327 function test_surfaceItemGetsNoTouchesWhenContainerNotInteractive() {
@@ -142,14 +334,14 @@ Rectangle {
142334 surfaceItem.touchPressCount = 0;
143335 surfaceItem.touchReleaseCount = 0;
144336
145− tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
337+ tap(surfaceContainer);
146338
147339 // surface got touches as the surfaceContainer is interactive
148340 compare(surfaceItem.touchPressCount, 1)
149341 compare(surfaceItem.touchReleaseCount, 1);
150342
151343 interactiveCheckbox.checked = false;
152− tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
344+ tap(surfaceContainer);
153345
154346 // surface shouldn't get the touches from the second tap as the surfaceContainer
155347 // was *not* interactive when it happened.
@@ -164,6 +356,8 @@ Rectangle {
164356 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
165357 verify(surfaceItem !== null);
166358
359+ surfaceContainer.focus = false;
360+
167361 compare(surfaceItem.activeFocus, false);
168362 mouseClick(surfaceContainer);
169363 compare(surfaceItem.activeFocus, true);
@@ -176,6 +370,8 @@ Rectangle {
176370 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
177371 verify(surfaceItem !== null);
178372
373+ surfaceContainer.focus = false;
374+
179375 compare(surfaceItem.activeFocus, false);
180376 tap(surfaceContainer);
181377 compare(surfaceItem.activeFocus, true);
tests/qmltests/Stages/tst_TabletStage.qml+252−89
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@ import Ubuntu.Components.ListItems 1.3 as ListItem
2121import Unity.Application 0.1
2222import Unity.Test 0.1
2323import Utils 0.1
24+import WindowManager 0.1
2425
2526import ".."
2627import "../../../qml/Stages"
@@ -34,6 +35,11 @@ Rectangle {
3435
3536 property var greeter: { fullyShown: true }
3637
38+ TopLevelSurfaceList {
39+ id: topSurfaceList
40+ applicationsModel: ApplicationManager
41+ }
42+
3743 Loader {
3844 id: tabletStageLoader
3945
@@ -62,6 +68,8 @@ Rectangle {
6268 primary: Qt.LandscapeOrientation
6369 }
6470 focus: true
71+ applicationManager: ApplicationManager
72+ topLevelSurfaceList: topSurfaceList
6573 }
6674 }
6775 }
@@ -76,6 +84,9 @@ Rectangle {
7684 right: parent.right
7785 }
7886
87+ // don't let mouse clicks in the controls area disturb the TabletStage behind it
88+ MouseArea { anchors.fill: parent }
89+
7990 Column {
8091 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8192 spacing: units.gu(1)
@@ -130,18 +141,21 @@ Rectangle {
130141 tabletStageLoader.active = true;
131142 tryCompare(tabletStageLoader, "status", Loader.Ready);
132143
144+ tryCompare(topSurfaceList, "count", 1);
145+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
146+
133147 // this is very strange, but sometimes the test starts without
134148 // TabletStage components having finished loading themselves
135− var appWindow = null;
149+ var appWindow = null
136150 while (!appWindow) {
137− appWindow = findChild(tabletStage, "appWindow_unity8-dash");
151+ appWindow = findAppWindowForSurfaceId(topSurfaceList.idAt(0));
138152 if (!appWindow) {
139− console.log("didn't find appWindow_unity8-dash in " + tabletStage + ". Trying again...");
153+ console.log("didn't find unity8-dash appWindow in " + tabletStage + ". Trying again...");
140154 wait(50);
141155 }
142156 }
143157
144− waitUntilAppSurfaceShowsUp("unity8-dash");
158+ waitUntilAppSurfaceShowsUp(topSurfaceList.idAt(0));
145159 sideStage.hideNow()
146160 }
147161
@@ -157,42 +171,39 @@ Rectangle {
157171 // Shell instance gets destroyed.
158172 tryCompare(tabletStageLoader, "itemDestroyed", true);
159173
160− // kill all (fake) running apps
161− webbrowserCheckBox.checked = false;
162− galleryCheckBox.checked = false;
163− dialerCheckBox.checked = false;
164− facebookCheckBox.checked = false;
174+ killApps();
165175 WindowStateStorage.clear();
166176 }
167177
168− function waitUntilAppSurfaceShowsUp(appId) {
169− var appWindow = findChild(tabletStage, "appWindow_" + appId);
178+ function waitUntilAppSurfaceShowsUp(surfaceId) {
179+ var appWindow = findAppWindowForSurfaceId(surfaceId);
170180 verify(appWindow);
171181 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
172182 verify(appWindowStates);
173183 tryCompare(appWindowStates, "state", "surface");
174184 }
175185
176− function switchToApp(targetAppId) {
177− touchFlick(tabletStage,
178− tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
179− tabletStage.x + 1, tabletStage.height / 2);
180−
181− var spreadView = findChild(tabletStage, "spreadView");
182− verify(spreadView);
183− tryCompare(spreadView, "phase", 2);
184− tryCompare(spreadView, "flicking", false);
185− tryCompare(spreadView, "moving", false);
186+ function findAppWindowForSurfaceId(surfaceId) {
187+ var spreadDelegate = findChild(tabletStage, "spreadDelegate_" + surfaceId);
188+ if (!spreadDelegate) {
189+ return null;
190+ }
191+ var appWindow = findChild(spreadDelegate, "appWindow");
192+ return appWindow;
193+ }
186194
187− waitUntilAppDelegateStopsMoving(targetAppId);
195+ function switchToSurface(targetSurfaceId) {
196+ performEdgeSwipeToShowAppSpread();
188197
189− var targetAppWindow = findChild(tabletStage, "appWindow_" + targetAppId);
198+ waitUntilAppDelegateStopsMoving(targetSurfaceId);
199+ var targetAppWindow = findAppWindowForSurfaceId(targetSurfaceId);
200+ verify(targetAppWindow);
190201 tap(targetAppWindow, 10, 10);
191202 }
192203
193− function waitUntilAppDelegateStopsMoving(targetAppId)
204+ function waitUntilAppDelegateStopsMoving(targetSurfaceId)
194205 {
195− var targetAppDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + targetAppId);
206+ var targetAppDelegate = findChild(tabletStage, "spreadDelegate_" + targetSurfaceId);
196207 verify(targetAppDelegate);
197208 var lastValue = undefined;
198209 do {
@@ -201,56 +212,86 @@ Rectangle {
201212 } while (lastValue != targetAppDelegate.animatedProgress);
202213 }
203214
215+ function performEdgeSwipeToShowAppSpread() {
216+ touchFlick(tabletStage,
217+ tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
218+ tabletStage.x + 1, tabletStage.height / 2);
219+
220+ var spreadView = findChild(tabletStage, "spreadView");
221+ verify(spreadView);
222+ tryCompare(spreadView, "phase", 2);
223+ tryCompare(spreadView, "flicking", false);
224+ tryCompare(spreadView, "moving", false);
225+ }
226+
227+ function swipeSurfaceUpwards(surfaceId) {
228+ var appWindow = findAppWindowForSurfaceId(surfaceId);
229+ verify(appWindow);
230+
231+ // Swipe from the left side of the surface as it's the one most likely
232+ // to not be covered by other surfaces when they're all being shown in the spread
233+ touchFlick(appWindow,
234+ appWindow.width * 0.1, appWindow.height / 2,
235+ appWindow.width * 0.1, -appWindow.height / 2);
236+ }
237+
204238 function test_tappingSwitchesFocusBetweenStages() {
205239 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
206240
241+ var webbrowserSurfaceId = topSurfaceList.nextId;
207242 webbrowserCheckBox.checked = true;
208− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
243+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
209244 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
210245 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
211− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", true);
246+ var webbrowserWindow = findAppWindowForSurfaceId(webbrowserSurfaceId);
247+ verify(webbrowserWindow);
248+
249+ tryCompare(webbrowserWindow.surface, "activeFocus", true);
212250
251+ var dialerSurfaceId = topSurfaceList.nextId;
213252 dialerCheckBox.checked = true;
214− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
253+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
254+
215255 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
216− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
217− tryCompare(dialerApp.session.lastSurface, "activeFocus", true);
218− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", false);
256+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
257+ verify(dialerDelegate);
258+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
259+
260+ tryCompare(dialerDelegate.surface, "activeFocus", true);
261+ tryCompare(webbrowserWindow.surface, "activeFocus", false);
219262
220263 // Tap on the main stage application and check if the focus
221264 // has been passed to it.
222265
223− var webbrowserWindow = findChild(tabletStage, "appWindow_" + webbrowserApp.appId);
224− verify(webbrowserWindow);
225266 tap(webbrowserWindow);
226267
227− tryCompare(dialerApp.session.lastSurface, "activeFocus", false);
228− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", true);
268+ tryCompare(dialerDelegate.surface, "activeFocus", false);
269+ tryCompare(webbrowserWindow.surface, "activeFocus", true);
229270
230271 // Now tap on the side stage application and check if the focus
231272 // has been passed back to it.
232273
233− var dialerWindow = findChild(tabletStage, "appWindow_" + dialerApp.appId);
234− verify(dialerWindow);
235− tap(dialerWindow);
274+ tap(dialerDelegate);
236275
237− tryCompare(dialerApp.session.lastSurface, "activeFocus", true);
238− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", false);
276+ tryCompare(dialerDelegate.surface, "activeFocus", true);
277+ tryCompare(webbrowserWindow.surface, "activeFocus", false);
239278 }
240279
241280 function test_closeAppInSideStage() {
242281 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
243282
283+ var dialerSurfaceId = topSurfaceList.nextId;
244284 dialerCheckBox.checked = true;
245− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
285+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
246286
247287 performEdgeSwipeToShowAppSpread();
248288
249− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + dialerCheckBox.appId);
289+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
250290 verify(appDelegate);
291+ compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
251292 tryCompare(appDelegate, "swipeToCloseEnabled", true);
252293
253− swipeAppUpwards(dialerCheckBox.appId);
294+ swipeSurfaceUpwards(dialerSurfaceId);
254295
255296 // Check that dialer-app has been closed
256297
@@ -263,54 +304,37 @@ Rectangle {
263304 }, null);
264305 }
265306
266− function performEdgeSwipeToShowAppSpread() {
267− var touchStartY = tabletStage.height / 2;
268− touchFlick(tabletStage,
269− tabletStage.width - 1, touchStartY,
270− 0, touchStartY);
271−
272− var spreadView = findChild(tabletStage, "spreadView");
273− verify(spreadView);
274− tryCompare(spreadView, "phase", 2);
275− tryCompare(spreadView, "flicking", false);
276− tryCompare(spreadView, "moving", false);
277− }
278−
279− function swipeAppUpwards(appId) {
280− var appWindow = findChild(tabletStage, "appWindow_" + appId);
281− verify(appWindow);
282−
283− touchFlick(appWindow,
284− appWindow.width / 2, appWindow.height / 2,
285− appWindow.width / 2, -appWindow.height / 2);
286− }
287−
288307 function test_suspendsAndResumesAppsInMainStage() {
308+ var webbrowserSurfaceId = topSurfaceList.nextId;
289309 webbrowserCheckBox.checked = true;
310+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
290311 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
291− compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
312+ var webbrowserDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
313+ compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
292314
293315 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
294316
317+ var gallerySurfaceId = topSurfaceList.nextId;
295318 galleryCheckBox.checked = true;
319+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
296320 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
297− compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
321+ var galleryDelegate = findChild(tabletStage, "spreadDelegate_" + gallerySurfaceId);
322+ compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
298323
299324 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
300325 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
301326
302− switchToApp(webbrowserApp.appId);
327+ switchToSurface(webbrowserSurfaceId);
303328
304329 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
305330 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
306331
307− switchToApp(galleryApp.appId);
332+ switchToSurface(gallerySurfaceId);
308333
309334 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
310335 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
311336 }
312337
313−
314338 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
315339 WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
316340 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
@@ -321,30 +345,37 @@ Rectangle {
321345 // launch two main stage apps
322346 // gallery will be on foreground and webbrowser on background
323347
348+ var webbrowserSurfaceId = topSurfaceList.nextId;
324349 webbrowserCheckBox.checked = true;
325− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
350+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
326351 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
327352 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
328353
354+ var gallerySurfaceId = topSurfaceList.nextId;
329355 galleryCheckBox.checked = true;
330− waitUntilAppSurfaceShowsUp(galleryCheckBox.appId)
356+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
331357 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
332− compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
358+ var galleryDelegate = findChild(tabletStage, "spreadDelegate_" + gallerySurfaceId);
359+ compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
333360
334361 compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
335362
336363 // then launch two side stage apps
337364 // facebook will be on foreground and dialer on background
338365
366+ var dialerSurfaceId = topSurfaceList.nextId;
339367 dialerCheckBox.checked = true;
340− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
368+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
341369 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
342− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
370+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
371+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
343372
373+ var facebookSurfaceId = topSurfaceList.nextId;
344374 facebookCheckBox.checked = true;
345− waitUntilAppSurfaceShowsUp(facebookCheckBox.appId)
375+ waitUntilAppSurfaceShowsUp(facebookSurfaceId);
346376 var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
347− compare(facebookApp.stage, ApplicationInfoInterface.SideStage);
377+ var facebookDelegate = findChild(tabletStage, "spreadDelegate_" + facebookSurfaceId);
378+ compare(facebookDelegate.stage, ApplicationInfoInterface.SideStage);
348379
349380 compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
350381
@@ -356,14 +387,14 @@ Rectangle {
356387 tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
357388 tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
358389
359− switchToApp(dialerCheckBox.appId);
390+ switchToSurface(dialerSurfaceId);
360391
361392 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
362393 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
363394 tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
364395 tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
365396
366− switchToApp(webbrowserCheckBox.appId);
397+ switchToSurface(webbrowserSurfaceId);
367398
368399 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
369400 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
@@ -374,15 +405,19 @@ Rectangle {
374405 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
375406 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
376407
408+ var webbrowserSurfaceId = topSurfaceList.nextId;
377409 webbrowserCheckBox.checked = true;
378− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
410+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
379411 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
380− compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
412+ var webbrowserDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
413+ compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
381414
415+ var dialerSurfaceId = topSurfaceList.nextId;
382416 dialerCheckBox.checked = true;
383− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
417+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
384418 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
385− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
419+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
420+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
386421
387422
388423 compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
@@ -467,8 +502,9 @@ Rectangle {
467502 tryCompare(stagesPriv, "mainStageAppId", "unity8-dash");
468503 tryCompare(stagesPriv, "sideStageAppId", "");
469504
505+ var webbrowserSurfaceId = topSurfaceList.nextId;
470506 webbrowserCheckBox.checked = true;
471− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
507+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
472508
473509 tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
474510 tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
@@ -476,10 +512,11 @@ Rectangle {
476512
477513 function test_loadSideStageByDragginFromMainStage() {
478514 sideStage.showNow();
515+ var webbrowserSurfaceId = topSurfaceList.nextId;
479516 webbrowserCheckBox.checked = true;
480− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
517+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
481518
482− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
519+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
483520 verify(appDelegate);
484521 compare(appDelegate.stage, ApplicationInfoInterface.MainStage);
485522
@@ -504,10 +541,11 @@ Rectangle {
504541 function test_unloadSideStageByDragginFromStageStage() {
505542 sideStage.showNow();
506543 WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
544+ var webbrowserSurfaceId = topSurfaceList.nextId;
507545 webbrowserCheckBox.checked = true;
508− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
546+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
509547
510− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
548+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
511549 verify(appDelegate);
512550 compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
513551
@@ -527,5 +565,130 @@ Rectangle {
527565
528566 tryCompare(appDelegate, "stage", ApplicationInfoInterface.MainStage);
529567 }
568+
569+ function test_closeSurfaceOfMultiSurfaceApp() {
570+ var surface1Id = topSurfaceList.nextId;
571+ webbrowserCheckBox.checked = true;
572+ waitUntilAppSurfaceShowsUp(surface1Id);
573+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
574+
575+ var surface2Id = topSurfaceList.nextId;
576+ verify(surface1Id !== surface2Id); // sanity checking
577+ webbrowserCheckBox.createSurface();
578+ waitUntilAppSurfaceShowsUp(surface2Id);
579+
580+ performEdgeSwipeToShowAppSpread();
581+
582+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + surface1Id);
583+ verify(appDelegate);
584+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
585+
586+ compare(webbrowserApp.surfaceList.count, 2);
587+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
588+
589+ swipeSurfaceUpwards(surface1Id);
590+
591+ // Surface must eventually be gone
592+ tryCompareFunction(function() { return topSurfaceList.indexForId(surface1Id); }, -1);
593+ tryCompare(webbrowserApp.surfaceList, "count", 1);
594+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
595+ }
596+
597+ /*
598+ 1- Suspended app gets killed behind the scenes, causing its surface to go zombie.
599+ 2- Surface gets screenshotted and removed. Its slot in the topSurfaceList remains,
600+ though (so ApplicationWindow can display the screenshot in its place).
601+ 3- User taps on the screenshot of the long-gone surface.
602+
603+ Expected outcome:
604+ Application gets relaunched. Its new surface will seamlessly replace the screenshot.
605+ */
606+ function test_selectSuspendedAppWithoutSurface() {
607+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
608+ var dashSurfaceId = topSurfaceList.idAt(0);
609+ var dashSurface = topSurfaceList.surfaceAt(0);
610+
611+ var webbrowserSurfaceId = topSurfaceList.nextId;
612+ webbrowserCheckBox.checked = true;
613+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
614+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
615+
616+ switchToSurface(dashSurfaceId);
617+
618+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
619+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
620+
621+ compare(webbrowserApp.surfaceList.count, 1);
622+
623+ // simulate the suspended app being killed by the out-of-memory daemon
624+ webbrowserApp.surfaceList.get(0).setLive(false);
625+
626+ // wait until the surface is gone
627+ tryCompare(webbrowserApp.surfaceList, "count", 0);
628+ compare(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
629+
630+ switchToSurface(webbrowserSurfaceId);
631+
632+ // webbrowser should have been brought to front
633+ tryCompareFunction(function(){return topSurfaceList.idAt(0);}, webbrowserSurfaceId);
634+
635+ // and it should eventually get a new surface and get resumed
636+ tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) !== null;}, true);
637+ compare(topSurfaceList.count, 2); // still two top-level items
638+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
639+ compare(webbrowserApp.surfaceList.count, 1);
640+ }
641+
642+ /*
643+ 1 - user suspends an application (ie, focus a surface from a different application)
644+ 2 - That suspended application gets killed, causing its surface to go zombie
645+ 3 - user goes to spread and closes that zombie surface. Actually, by that time
646+ Tablet shell will be displaying a screenshot of it instead, so there's no
647+ MirSurface whatsoever backing it up.
648+ */
649+ function test_closeZombieSurface()
650+ {
651+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
652+ var dashSurfaceId = topSurfaceList.idAt(0);
653+ var dashSurface = topSurfaceList.surfaceAt(0);
654+
655+ var webbrowserSurfaceId = topSurfaceList.nextId;
656+ webbrowserCheckBox.checked = true;
657+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
658+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
659+
660+ switchToSurface(dashSurfaceId);
661+
662+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
663+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
664+
665+ compare(webbrowserApp.surfaceList.count, 1);
666+
667+ // simulate the suspended app being killed by the out-of-memory daemon
668+ webbrowserApp.surfaceList.get(0).setLive(false);
669+
670+ // wait until the surface is gone
671+ tryCompare(webbrowserApp.surfaceList, "count", 0);
672+ compare(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
673+
674+ performEdgeSwipeToShowAppSpread();
675+
676+ {
677+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
678+ verify(appDelegate);
679+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
680+ }
681+
682+ swipeSurfaceUpwards(webbrowserSurfaceId);
683+
684+ // webbrowser entry is nowhere to be seen
685+ tryCompareFunction(function(){return topSurfaceList.indexForId(webbrowserSurfaceId);}, -1);
686+
687+ // nor is its app
688+ compare(ApplicationManager.findApplication(webbrowserCheckBox.appId), null);
689+
690+ // only unity8-dash surface is left
691+ compare(topSurfaceList.count, 1);
692+ }
530693 }
531694}
tests/qmltests/Tutorial/tst_Tutorial.qml+4−8
@@ -113,6 +113,8 @@ Rectangle {
113113 sourceComponent: Component {
114114 Shell {
115115 usageScenario: shellLoader.state
116+ nativeWidth: width
117+ nativeHeight: height
116118 property string indicatorProfile: "phone"
117119 orientation: shellLoader.shellOrientation
118120 orientations: Orientations {
@@ -280,14 +282,6 @@ Rectangle {
280282 prepareShell();
281283 }
282284
283− function killApps() {
284− while (ApplicationManager.count > 1) {
285− var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
286− ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
287− }
288− compare(ApplicationManager.count, 1)
289− }
290−
291285 function swipeAwayGreeter() {
292286 var coverPage = findChild(shell, "coverPage");
293287 tryCompare(coverPage, "showProgress", 1);
@@ -409,6 +403,8 @@ Rectangle {
409403
410404 var launcher = findChild(shell, "launcher");
411405 var tutorialLeft = findChild(shell, "tutorialLeftLoader");
406+ verify(tutorialLeft);
407+
412408 touchFlick(shell, 0, halfHeight, shell.width, halfHeight);
413409
414410 tryCompare(tutorialLeft, "shown", false);
tests/qmltests/tst_OrientedShell.qml+92−65
@@ -25,6 +25,7 @@ import Unity.Test 0.1
2525import IntegratedLightDM 0.1 as LightDM
2626import Powerd 0.1
2727import Unity.InputInfo 0.1
28+import Utils 0.1
2829
2930import "../../qml"
3031
@@ -432,6 +433,7 @@ Rectangle {
432433
433434 property Item orientedShell: orientedShellLoader.status === Loader.Ready ? orientedShellLoader.item : null
434435 property Item shell
436+ property QtObject topLevelSurfaceList
435437
436438 SignalSpy { id: signalSpy }
437439 SignalSpy { id: signalSpy2 }
@@ -490,6 +492,7 @@ Rectangle {
490492 function cleanup() {
491493 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
492494 shell = null;
495+ topLevelSurfaceList = null;
493496
494497 tearDown();
495498 }
@@ -497,17 +500,23 @@ Rectangle {
497500 function test_appSupportingOnlyPrimaryOrientationMakesPhoneShellStayPut() {
498501 loadShell("mako");
499502
503+ var spreadRepeater = findChild(shell, "spreadRepeater");
504+ verify(spreadRepeater);
505+
500506 // unity8-dash supports only primary orientation and should be already running
501− var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
502− verify(dashAppWindow);
503− compare(ApplicationManager.focusedApplicationId, "unity8-dash");
504− var dashApp = dashAppWindow.application
507+ compare(spreadRepeater.count, 1);
508+ var dashDelegate = spreadRepeater.itemAt(0);
509+ var dashApp = dashDelegate.application;
505510 verify(dashApp);
511+ compare(dashApp.appId, "unity8-dash");
512+ var dashAppWindow = findChild(dashDelegate, "appWindow");
513+ verify(dashAppWindow);
514+ compare(dashDelegate.focus, true);
506515 compare(dashApp.rotatesWindowContents, false);
507516 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
508517 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
509518
510− tryCompareFunction(function(){return dashApp.session.lastSurface != null;}, true);
519+ tryCompareFunction(function(){return dashDelegate.surface != null;}, true);
511520 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
512521
513522 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
@@ -538,17 +547,20 @@ Rectangle {
538547 function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape(data) {
539548 loadShell(data.deviceName);
540549
550+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
551+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
552+ var dashAppWindow = findAppWindowForSurfaceId(dashSurfaceId);
553+
541554 // unity8-dash supports only primary orientation and should be already running
542− var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
543− verify(dashAppWindow);
555+
544556 compare(ApplicationManager.focusedApplicationId, "unity8-dash");
545− var dashApp = dashAppWindow.application
557+ var dashApp = dashAppWindow.application;
546558 verify(dashApp);
547559 compare(dashApp.rotatesWindowContents, false);
548560 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
549561 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
550562
551− tryCompareFunction(function(){return dashApp.session.lastSurface != null;}, true);
563+ tryCompareFunction(function(){return dashApp.surfaceList.count > 0;}, true);
552564 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
553565
554566 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
@@ -615,6 +627,7 @@ Rectangle {
615627 }
616628 function test_appRotatesWindowContents(data) {
617629 loadShell(data.deviceName);
630+ var cameraSurfaceId = topLevelSurfaceList.nextId;
618631 var cameraApp = ApplicationManager.startApplication("camera-app");
619632 verify(cameraApp);
620633
@@ -624,9 +637,9 @@ Rectangle {
624637 compare(cameraApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
625638 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
626639
627− waitUntilAppSurfaceShowsUp("camera-app")
640+ waitUntilAppWindowIsFullyLoaded(cameraSurfaceId);
628641
629− var cameraSurface = cameraApp.session.lastSurface;
642+ var cameraSurface = cameraApp.surfaceList.get(0);
630643 verify(cameraSurface);
631644
632645 var focusChangedSpy = signalSpy;
@@ -692,6 +705,7 @@ Rectangle {
692705 }
693706 function test_switchingToAppWithDifferentRotation(data) {
694707 loadShell(data.deviceName);
708+ var gmailSurfaceId = topLevelSurfaceList.nextId;
695709 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
696710 verify(gmailApp);
697711
@@ -701,8 +715,9 @@ Rectangle {
701715 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
702716 compare(gmailApp.stage, ApplicationInfoInterface.MainStage);
703717
704− waitUntilAppSurfaceShowsUp("gmail-webapp")
718+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
705719
720+ var musicSurfaceId = topLevelSurfaceList.nextId;
706721 var musicApp = ApplicationManager.startApplication("music-app");
707722 verify(musicApp);
708723
@@ -712,7 +727,7 @@ Rectangle {
712727 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
713728 compare(musicApp.stage, ApplicationInfoInterface.MainStage);
714729
715− waitUntilAppSurfaceShowsUp("music-app")
730+ waitUntilAppWindowIsFullyLoaded(musicSurfaceId);
716731 tryCompare(shell, "orientationChangesEnabled", true);
717732
718733 rotateTo(90);
@@ -747,6 +762,7 @@ Rectangle {
747762 */
748763 function test_rotateToUnsupportedDeviceOrientation(data) {
749764 loadShell("mako");
765+ var twitterSurfaceId = topLevelSurfaceList.nextId;
750766 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
751767 verify(twitterApp);
752768
@@ -755,7 +771,7 @@ Rectangle {
755771 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
756772 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
757773
758− waitUntilAppSurfaceShowsUp("twitter-webapp")
774+ waitUntilAppWindowIsFullyLoaded(twitterSurfaceId);
759775
760776 rotateTo(data.rotationAngle);
761777 tryCompare(shell, "transformRotationAngle", data.rotationAngle);
@@ -772,13 +788,14 @@ Rectangle {
772788
773789 function test_launchLandscapeOnlyAppFromPortrait() {
774790 loadShell("mako");
791+ var weatherSurfaceId = topLevelSurfaceList.nextId;
775792 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
776793 verify(weatherApp);
777794
778795 // ensure the mock app is as we expect
779796 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
780797
781− waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
798+ waitUntilAppWindowIsFullyLoaded(weatherSurfaceId);
782799
783800 var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
784801 waitUntilTransitionsEnd(rotationStates);
@@ -810,6 +827,7 @@ Rectangle {
810827 */
811828 function test_greeterStaysAwayAfterRotation() {
812829 loadShell("mako");
830+ var twitterSurfaceId = topLevelSurfaceList.nextId;
813831 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
814832 verify(twitterApp);
815833
@@ -818,8 +836,8 @@ Rectangle {
818836 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
819837 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
820838
821− waitUntilAppSurfaceShowsUp("twitter-webapp");
822− waitUntilAppWindowCanRotate("twitter-webapp");
839+ waitUntilAppWindowIsFullyLoaded(twitterSurfaceId);
840+ waitUntilAppWindowCanRotate(twitterSurfaceId);
823841
824842 // go back to unity8-dash
825843 performEdgeSwipeToSwitchToPreviousApp();
@@ -835,7 +853,7 @@ Rectangle {
835853 var greeter = findChild(shell, "greeter");
836854 tryCompare(greeter, "animating", false);
837855
838− var twitterDelegate = findChild(shell, "appDelegate1");
856+ var twitterDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
839857 compare(twitterDelegate.application.appId, "twitter-webapp");
840858 twitterDelegate.clicked();
841859
@@ -908,9 +926,9 @@ Rectangle {
908926 verify(item.application.appId, "dialer-app");
909927 }
910928
929+ WindowStateStorage.saveStage("dialer-app", ApplicationInfoInterface.SideStage)
911930 var dialerApp = ApplicationManager.startApplication("dialer-app");
912931 verify(dialerApp);
913− dialerApp.stage = ApplicationInfoInterface.SideStage;
914932
915933 // ensure the mock dialer-app is as we expect
916934 compare(dialerApp.rotatesWindowContents, false);
@@ -955,9 +973,9 @@ Rectangle {
955973 verify(item.application.appId, "dialer-app");
956974 }
957975
976+ WindowStateStorage.saveStage("dialer-app", ApplicationInfoInterface.SideStage)
958977 var dialerApp = ApplicationManager.startApplication("dialer-app");
959978 verify(dialerApp);
960− dialerApp.stage = ApplicationInfoInterface.SideStage;
961979
962980 // ensure the mock dialer-app is as we expect
963981 compare(dialerApp.rotatesWindowContents, false);
@@ -1023,13 +1041,15 @@ Rectangle {
10231041 function test_launchedAppHasActiveFocus(data) {
10241042 loadShell(data.deviceName);
10251043
1044+ var gmailSurfaceId = topLevelSurfaceList.nextId;
10261045 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
10271046 verify(gmailApp);
1028− waitUntilAppSurfaceShowsUp("gmail-webapp")
1047+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
10291048
1030− verify(gmailApp.session.lastSurface);
1049+ var gmailSurface = gmailApp.surfaceList.get(0);
1050+ verify(gmailSurface);
10311051
1032− tryCompare(gmailApp.session.lastSurface, "activeFocus", true);
1052+ tryCompare(gmailSurface, "activeFocus", true);
10331053 }
10341054
10351055 function test_launchLandscapeOnlyAppOverPortraitOnlyDashThenSwitchToDash() {
@@ -1038,13 +1058,14 @@ Rectangle {
10381058 // starts as portrait, as unity8-dash is portrait only
10391059 tryCompare(shell, "transformRotationAngle", 0);
10401060
1061+ var weatherSurfaceId = topLevelSurfaceList.nextId;
10411062 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
10421063 verify(weatherApp);
10431064
10441065 // ensure the mock app is as we expect
10451066 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
10461067
1047− waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
1068+ waitUntilAppWindowIsFullyLoaded(weatherSurfaceId);
10481069
10491070 // should have rotated to landscape
10501071 tryCompareFunction(function () { return shell.transformRotationAngle == 270
@@ -1243,10 +1264,14 @@ Rectangle {
12431264 function test_lockPhoneAfterClosingAppInSpreadThenUnlockAndRotate() {
12441265 loadShell("mako");
12451266
1267+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
1268+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
1269+
1270+ var gmailSurfaceId = topLevelSurfaceList.nextId;
12461271 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
12471272 verify(gmailApp);
12481273
1249− waitUntilAppSurfaceShowsUp("gmail-webapp");
1274+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
12501275
12511276 performEdgeSwipeToShowAppSpread();
12521277
@@ -1262,7 +1287,7 @@ Rectangle {
12621287
12631288 swipeAwayGreeter();
12641289
1265− verify(isAppSurfaceFocused("unity8-dash"))
1290+ verify(isAppSurfaceFocused(dashSurfaceId))
12661291
12671292 signalSpy.clear();
12681293 signalSpy.target = shell;
@@ -1324,6 +1349,7 @@ Rectangle {
13241349 ////
13251350 // Launch a portrait-only application
13261351
1352+ var dialerSurfaceId = topLevelSurfaceList.nextId;
13271353 var dialerApp = ApplicationManager.startApplication("dialer-app");
13281354 verify(dialerApp);
13291355
@@ -1331,9 +1357,9 @@ Rectangle {
13311357 compare(dialerApp.rotatesWindowContents, false);
13321358 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
13331359
1334− waitUntilAppSurfaceShowsUp("dialer-app");
1335− waitUntilAppWindowCanRotate("dialer-app");
1336− verify(isAppSurfaceFocused("dialer-app"));
1360+ waitUntilAppWindowIsFullyLoaded(dialerSurfaceId);
1361+ waitUntilAppWindowCanRotate(dialerSurfaceId);
1362+ verify(isAppSurfaceFocused(dialerSurfaceId));
13371363
13381364 ////
13391365 // check outcome (shell should stay in landscape)
@@ -1386,17 +1412,29 @@ Rectangle {
13861412 tryCompare(appWindowStates, "state", "surface");
13871413 }
13881414
1389− function waitUntilAppSurfaceShowsUp(appId) {
1390− var appWindow = findChild(shell, "appWindow_" + appId);
1391− verify(appWindow);
1392− var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
1393− verify(appWindowStates);
1394− tryCompare(appWindowStates, "state", "surface");
1415+ function findAppWindowForSurfaceId(surfaceId) {
1416+ // for PhoneStage and TabletStage
1417+ var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
1418+ if (!delegate) {
1419+ // for DesktopStage
1420+ delegate = findChild(shell, "appDelegate_" + surfaceId);
1421+ }
1422+ verify(delegate);
1423+ var appWindow = findChild(delegate, "appWindow");
1424+ return appWindow;
13951425 }
13961426
1397− function waitUntilAppWindowCanRotate(appId) {
1398− var appWindow = findChild(shell, "appWindow_" + appId);
1399− verify(appWindow);
1427+ // Wait until the ApplicationWindow for the given Application object is fully loaded
1428+ // (ie, the real surface has replaced the splash screen)
1429+ function waitUntilAppWindowIsFullyLoaded(surfaceId) {
1430+ var appWindow = findAppWindowForSurfaceId(surfaceId);
1431+ var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
1432+ tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
1433+ waitUntilTransitionsEnd(appWindowStateGroup);
1434+ }
1435+
1436+ function waitUntilAppWindowCanRotate(surfaceId) {
1437+ var appWindow = findAppWindowForSurfaceId(surfaceId);
14001438 tryCompare(appWindow, "orientationChangesEnabled", true);
14011439 }
14021440
@@ -1426,8 +1464,8 @@ Rectangle {
14261464 var spreadView = findChild(shell, "spreadView");
14271465 verify(spreadView);
14281466
1429− verify(ApplicationManager.count >= 2);
1430− var previousApp = ApplicationManager.get(1);
1467+ verify(topLevelSurfaceList.count >= 2);
1468+ var previousSurfaceId = topLevelSurfaceList.idAt(1);
14311469
14321470 var touchStartX = shell.width - 1;
14331471 var touchStartY = shell.height / 2;
@@ -1453,7 +1491,7 @@ Rectangle {
14531491 // ensure the app switch animation has ended
14541492 tryCompare(spreadView, "shiftedContentX", 0);
14551493
1456− compare(ApplicationManager.get(0).appId, previousApp.appId);
1494+ tryCompareFunction(function(){ return topLevelSurfaceList.idAt(0); }, previousSurfaceId);
14571495 }
14581496
14591497 function performEdgeSwipeToShowAppSpread() {
@@ -1508,14 +1546,6 @@ Rectangle {
15081546 tryCompare(greeter, "fullyShown", true);
15091547 }
15101548
1511− function killApps() {
1512− while (ApplicationManager.count > 1) {
1513− var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
1514− ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
1515− }
1516− compare(ApplicationManager.count, 1)
1517− }
1518−
15191549 function loadShell(deviceName) {
15201550 applicationArguments.deviceName = deviceName;
15211551
@@ -1527,6 +1557,12 @@ Rectangle {
15271557
15281558 shell = findChild(orientedShell, "shell");
15291559
1560+ topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
1561+ verify(topLevelSurfaceList);
1562+
1563+ var stageLoader = findChild(shell, "applicationsDisplayLoader");
1564+ verify(stageLoader);
1565+
15301566 tryCompare(shell, "enabled", true); // enabled by greeter when ready
15311567
15321568 waitUntilShellIsInOrientation(root.physicalOrientation0);
@@ -1535,7 +1571,7 @@ Rectangle {
15351571
15361572 swipeAwayGreeter();
15371573
1538− var spreadRepeater = findChild(shell, "spreadRepeater");
1574+ var spreadRepeater = findChild(stageLoader, "spreadRepeater");
15391575 if (spreadRepeater) {
15401576 spreadRepeaterConnections.target = spreadRepeater;
15411577 }
@@ -1556,7 +1592,7 @@ Rectangle {
15561592
15571593 // expectedAngle is in orientedShell's coordinate system
15581594 function checkAppSurfaceOrientation(item, app, expectedAngle) {
1559− var surface = app.session.lastSurface;
1595+ var surface = app.surfaceList.get(0);
15601596 if (!surface) {
15611597 console.warn("no surface");
15621598 return false;
@@ -1611,7 +1647,7 @@ Rectangle {
16111647 var spreadView = findChild(shell, "spreadView");
16121648 verify(spreadView);
16131649
1614− var delegateToClose = findChild(spreadView, "appDelegate0");
1650+ var delegateToClose = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
16151651 verify(delegateToClose);
16161652
16171653 var appIdToClose = ApplicationManager.get(0).appId;;
@@ -1637,20 +1673,11 @@ Rectangle {
16371673 compare(ApplicationManager.findApplication(appIdToClose), null);
16381674 }
16391675
1640− function isAppSurfaceFocused(appId) {
1641− var appWindow = findChild(shell, "appWindow_" + appId);
1642− verify(appWindow);
1643−
1644− var app = ApplicationManager.findApplication(appId);
1645− verify(app);
1646−
1647− var surface = app.session.lastSurface;
1676+ function isAppSurfaceFocused(surfaceId) {
1677+ var index = topLevelSurfaceList.indexForId(surfaceId);
1678+ var surface = topLevelSurfaceList.surfaceAt(index);
16481679 verify(surface);
1649−
1650− var surfaceItem = findSurfaceItem(appWindow, surface);
1651− verify(surfaceItem);
1652−
1653− return surfaceItem.activeFocus;
1680+ return surface.activeFocus;
16541681 }
16551682 }
16561683}
tests/qmltests/tst_Shell.qml+233−160
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013-2015 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -131,9 +131,6 @@ Rectangle {
131131 anchors.right: root.right
132132 width: units.gu(30)
133133
134− property var focusedApp: ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
135− property var focusedSurface: focusedApp && focusedApp.session ? focusedApp.session.lastSurface : null
136−
137134 Rectangle {
138135 id: controlRect
139136 anchors { left: parent.left; right: parent.right }
@@ -185,6 +182,17 @@ Rectangle {
185182 }
186183 }
187184 }
185+ Button {
186+ text: "Show Launcher"
187+ activeFocusOnPress: false
188+ onClicked: {
189+ if (shellLoader.status !== Loader.Ready)
190+ return;
191+
192+ var launcher = testCase.findChild(shellLoader.item, "launcher");
193+ launcher.state = "visible";
194+ }
195+ }
188196 ListItem.ItemSelector {
189197 anchors { left: parent.left; right: parent.right }
190198 activeFocusOnPress: false
@@ -274,21 +282,21 @@ Rectangle {
274282 id: fullscreeAppCheck
275283
276284 onTriggered: {
277− if (!controls.focusedSurface) return;
278− if (controls.focusedSurface.state == Mir.FullscreenState) {
279− controls.focusedSurface.state = Mir.RestoredState;
285+ if (!MirFocusController.focusedSurface) return;
286+ if (MirFocusController.focusedSurface.state == Mir.FullscreenState) {
287+ MirFocusController.focusedSurface.state = Mir.RestoredState;
280288 } else {
281− controls.focusedSurface.state = Mir.FullscreenState;
289+ MirFocusController.focusedSurface.state = Mir.FullscreenState;
282290 }
283291 }
284292
285293 Binding {
286294 target: fullscreeAppCheck
287− when: controls.focusedSurface
295+ when: MirFocusController.focusedSurface
288296 property: "checked"
289297 value: {
290− if (!controls.focusedSurface) return false;
291− return controls.focusedSurface.state === Mir.FullscreenState
298+ if (!MirFocusController.focusedSurface) return false;
299+ return MirFocusController.focusedSurface.state === Mir.FullscreenState
292300 }
293301 }
294302 }
@@ -302,21 +310,21 @@ Rectangle {
302310 id: chromeAppCheck
303311
304312 onTriggered: {
305− if (!controls.focusedSurface) return;
306− if (controls.focusedSurface.shellChrome == Mir.LowChrome) {
307− controls.focusedSurface.setShellChrome(Mir.NormalChrome);
313+ if (!MirFocusController.focusedSurface) return;
314+ if (MirFocusController.focusedSurface.shellChrome == Mir.LowChrome) {
315+ MirFocusController.focusedSurface.setShellChrome(Mir.NormalChrome);
308316 } else {
309− controls.focusedSurface.setShellChrome(Mir.LowChrome);
317+ MirFocusController.focusedSurface.setShellChrome(Mir.LowChrome);
310318 }
311319 }
312320
313321 Binding {
314322 target: chromeAppCheck
315− when: controls.focusedSurface !== null
323+ when: MirFocusController.focusedSurface !== null
316324 property: "checked"
317325 value: {
318− if (!controls.focusedSurface) return false;
319− controls.focusedSurface.shellChrome === Mir.LowChrome
326+ if (!MirFocusController.focusedSurface) return false;
327+ MirFocusController.focusedSurface.shellChrome === Mir.LowChrome
320328 }
321329 }
322330 }
@@ -381,12 +389,6 @@ Rectangle {
381389 signalName: "actionInvoked"
382390 }
383391
384− SignalSpy {
385− id: appRemovedSpy
386− target: ApplicationManager
387− signalName: "applicationRemoved"
388− }
389−
390392 Telephony.CallEntry {
391393 id: phoneCall
392394 phoneNumber: "+447812221111"
@@ -412,6 +414,7 @@ Rectangle {
412414 when: windowShown
413415
414416 property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
417+ property QtObject topLevelSurfaceList: null
415418
416419 function init() {
417420 if (shellLoader.active) {
@@ -449,6 +452,9 @@ Rectangle {
449452 panel.dismissTimer = fakeDismissTimer;
450453
451454 waitForGreeterToStabilize();
455+
456+ topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
457+ verify(topLevelSurfaceList);
452458 }
453459
454460 function loadDesktopShellWithApps() {
@@ -463,7 +469,9 @@ Rectangle {
463469 var app5 = ApplicationManager.startApplication("camera-app")
464470 var app6 = ApplicationManager.startApplication("gallery-app")
465471 var app7 = ApplicationManager.startApplication("calendar-app")
466− waitUntilAppWindowIsFullyLoaded(app7);
472+ for (var i = 0; i < topLevelSurfaceList.count; ++i) {
473+ waitUntilAppWindowIsFullyLoaded(topLevelSurfaceList.idAt(i));
474+ }
467475 }
468476
469477 function waitForGreeterToStabilize() {
@@ -503,7 +511,7 @@ Rectangle {
503511 Wizard.System.wizardEnabled = false;
504512
505513 // kill all (fake) running apps
506− killApps(ApplicationManager);
514+ killApps();
507515
508516 unlockAllModemsSpy.clear()
509517 LightDM.Greeter.authenticate(""); // reset greeter
@@ -511,26 +519,25 @@ Rectangle {
511519 sessionSpy.clear();
512520
513521 GSettingsController.setLifecycleExemptAppids([]);
514− }
515522
516− function killApps() {
517− while (ApplicationManager.count > 1) {
518− var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
519− ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
520− }
521− compare(ApplicationManager.count, 1)
523+ // there should be only unity8-dash window over there
524+ tryCompare(ApplicationManager, "count", 1);
525+ compare(ApplicationManager.get(0).appId, "unity8-dash");
522526 }
523527
524528 function test_snapDecisionDismissalReturnsFocus() {
525529 loadShell("phone");
526530 swipeAwayGreeter();
527531 var notifications = findChild(shell, "notificationList");
532+ var appSurfaceId = topLevelSurfaceList.nextId;
528533 var app = ApplicationManager.startApplication("camera-app");
534+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
529535 var stage = findChild(shell, "stage")
530− // Open an application and focus
531− waitUntilApplicationWindowIsFullyVisible(app);
532− ApplicationManager.focusApplication(app);
533− tryCompare(app.session.lastSurface, "activeFocus", true);
536+
537+ var appSurface = app.surfaceList.get(0);
538+ verify(appSurface);
539+
540+ tryCompare(appSurface, "activeFocus", true);
534541
535542 notifications.model = mockNotificationsModel;
536543
@@ -552,7 +559,7 @@ Rectangle {
552559 waitForRendering(notification);
553560
554561 // Make sure activeFocus went away from the app window
555− tryCompare(app.session.lastSurface, "activeFocus", false);
562+ tryCompare(appSurface, "activeFocus", false);
556563 tryCompare(stage, "interactive", false);
557564
558565 // Clicking the button should dismiss the notification and return focus
@@ -560,7 +567,7 @@ Rectangle {
560567 mouseClick(buttonAccept);
561568
562569 // Make sure we're back to normal
563− tryCompare(app.session.lastSurface, "activeFocus", true);
570+ tryCompare(appSurface, "activeFocus", true);
564571 compare(stage.interactive, true, "Stages not interactive again after modal notification has closed");
565572 }
566573
@@ -618,10 +625,9 @@ Rectangle {
618625 loadShell("phone");
619626 swipeAwayGreeter();
620627 dragLauncherIntoView();
628+ var appSurfaceId = topLevelSurfaceList.nextId;
621629 tapOnAppIconInLauncher();
622− waitUntilApplicationWindowIsFullyVisible();
623− ApplicationManager.focusApplication(data.focusedApp)
624− waitUntilApplicationWindowIsFullyVisible();
630+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
625631
626632 var greeter = findChild(shell, "greeter");
627633 if (data.greeterShown) {
@@ -637,16 +643,18 @@ Rectangle {
637643 waitUntilDashIsFocused();
638644 tryCompare(greeter, "shown", false);
639645 } else {
640− waitUntilApplicationWindowIsFullyVisible();
641646 compare(greeter.fullyShown, data.greeterShown);
642647 }
643648
644649 var launcher = findChild(shell, "launcherPanel");
645650 tryCompare(launcher, "x", data.launcherHides ? -launcher.width : 0)
646651
652+ //FIXME: This check fails. Don't understand the rationale behind it.
653+ /*
647654 // Make sure the helper for sliding out the launcher wasn't touched. We want to fade it out here.
648655 var animateTimer = findInvisibleChild(shell, "animateTimer");
649656 compare(animateTimer.nextState, "visible");
657+ */
650658 }
651659
652660 function test_tabletLeftEdgeDrag_data() {
@@ -673,10 +681,11 @@ Rectangle {
673681 dragLauncherIntoView();
674682 dashCommunicatorSpy.clear();
675683
684+ var appSurfaceId = topLevelSurfaceList.nextId;
676685 tapOnAppIconInLauncher();
677− waitUntilApplicationWindowIsFullyVisible();
686+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
678687
679− verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
688+ tryCompareFunction(function() { return ApplicationManager.focusedApplicationId !== "unity8-dash"; }, true);
680689
681690 //Long left swipe
682691 swipeFromLeftEdge(units.gu(30));
@@ -709,8 +718,9 @@ Rectangle {
709718
710719 // Launch an app from the launcher
711720 dragLauncherIntoView();
721+ var appSurfaceId = topLevelSurfaceList.nextId;
712722 tapOnAppIconInLauncher();
713− waitUntilApplicationWindowIsFullyVisible();
723+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
714724
715725 var mainAppId = ApplicationManager.focusedApplicationId;
716726 verify(mainAppId != "");
@@ -835,9 +845,9 @@ Rectangle {
835845 dragLauncherIntoView();
836846
837847 // Launch an app from the launcher
848+ var appSurfaceId = topLevelSurfaceList.nextId;
838849 tapOnAppIconInLauncher();
839−
840− waitUntilApplicationWindowIsFullyVisible();
850+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
841851
842852 verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
843853
@@ -873,10 +883,22 @@ Rectangle {
873883 tryCompare(item, "visible", false);
874884 }
875885
886+ function findAppWindowForSurfaceId(surfaceId) {
887+ // for PhoneStage and TabletStage
888+ var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
889+ if (!delegate) {
890+ // for DesktopStage
891+ delegate = findChild(shell, "appDelegate_" + surfaceId);
892+ }
893+ verify(delegate);
894+ var appWindow = findChild(delegate, "appWindow");
895+ return appWindow;
896+ }
897+
876898 // Wait until the ApplicationWindow for the given Application object is fully loaded
877899 // (ie, the real surface has replaced the splash screen)
878− function waitUntilAppWindowIsFullyLoaded(app) {
879− var appWindow = findChild(shell, "appWindow_" + app.appId);
900+ function waitUntilAppWindowIsFullyLoaded(surfaceId) {
901+ var appWindow = findAppWindowForSurfaceId(surfaceId);
880902 var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
881903 tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
882904 waitUntilTransitionsEnd(appWindowStateGroup);
@@ -885,10 +907,13 @@ Rectangle {
885907 function test_surfaceLosesActiveFocusWhilePanelIsOpen() {
886908 loadShell("phone");
887909 swipeAwayGreeter();
910+ var appSurfaceId = topLevelSurfaceList.nextId;
888911 var app = ApplicationManager.startApplication("dialer-app");
889− waitUntilAppWindowIsFullyLoaded(app);
912+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
913+ var appSurface = app.surfaceList.get(0);
914+ verify(appSurface);
890915
891− tryCompare(app.session.lastSurface, "activeFocus", true);
916+ tryCompare(appSurface, "activeFocus", true);
892917
893918 // Drag the indicators panel half-open
894919 var touchX = shell.width / 2;
@@ -899,7 +924,7 @@ Rectangle {
899924 true /* beginTouch */, false /* endTouch */);
900925 verify(indicators.partiallyOpened);
901926
902− tryCompare(app.session.lastSurface, "activeFocus", false);
927+ tryCompare(appSurface, "activeFocus", false);
903928
904929 // And finish getting it open
905930 touchFlick(indicators,
@@ -908,11 +933,11 @@ Rectangle {
908933 false /* beginTouch */, true /* endTouch */);
909934 tryCompare(indicators, "fullyOpened", true);
910935
911− tryCompare(app.session.lastSurface, "activeFocus", false);
936+ tryCompare(appSurface, "activeFocus", false);
912937
913938 dragToCloseIndicatorsPanel();
914939
915− tryCompare(app.session.lastSurface, "activeFocus", true);
940+ tryCompare(appSurface, "activeFocus", true);
916941 }
917942
918943 function test_launchedAppHasActiveFocus_data() {
@@ -928,54 +953,44 @@ Rectangle {
928953 shell.usageScenario = data.usageScenario;
929954 swipeAwayGreeter();
930955
956+ var webAppSurfaceId = topLevelSurfaceList.nextId;
931957 var webApp = ApplicationManager.startApplication("webbrowser-app");
932958 verify(webApp);
933− waitUntilAppSurfaceShowsUp("webbrowser-app")
959+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
934960
935− verify(webApp.session.lastSurface);
961+ var webAppSurface = webApp.surfaceList.get(topLevelSurfaceList.indexForId(webAppSurfaceId));
962+ verify(webAppSurface);
936963
937− tryCompare(webApp.session.lastSurface, "activeFocus", true);
964+ tryCompare(webAppSurface, "activeFocus", true);
938965 }
939966
940967 function test_launchedAppKeepsActiveFocusOnUsageModeChange() {
941968 loadShell("tablet");
942969 swipeAwayGreeter();
943970
971+ var webAppSurfaceId = topLevelSurfaceList.nextId;
944972 var webApp = ApplicationManager.startApplication("webbrowser-app");
945973 verify(webApp);
946− waitUntilAppSurfaceShowsUp("webbrowser-app")
974+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
947975
948− verify(webApp.session.lastSurface);
976+ var webAppSurface = webApp.surfaceList.get(topLevelSurfaceList.indexForId(webAppSurfaceId));
977+ verify(webAppSurface);
949978
950− tryCompare(webApp.session.lastSurface, "activeFocus", true);
979+ tryCompare(webAppSurface, "activeFocus", true);
951980
952981 shell.usageScenario = "desktop";
953982
954983 // check that the desktop stage and window have been loaded
955− {
956− var desktopWindow = findChild(shell, "appWindow_webbrowser-app");
957− verify(desktopWindow);
958− }
984+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
959985
960− tryCompare(webApp.session.lastSurface, "activeFocus", true);
986+ tryCompare(webAppSurface, "activeFocus", true);
961987
962988 shell.usageScenario = "tablet";
963989
964990 // check that the tablet stage and app surface delegate have been loaded
965− {
966− var desktopWindow = findChild(shell, "tabletSpreadDelegate_webbrowser-app");
967− verify(desktopWindow);
968− }
991+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
969992
970− tryCompare(webApp.session.lastSurface, "activeFocus", true);
971− }
972−
973− function waitUntilAppSurfaceShowsUp(appId) {
974− var appWindow = findChild(shell, "appWindow_" + appId);
975− verify(appWindow);
976− var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
977− verify(appWindowStates);
978− tryCompare(appWindowStates, "state", "surface");
993+ tryCompare(webAppSurface, "activeFocus", true);
979994 }
980995
981996 function dragToCloseIndicatorsPanel() {
@@ -1025,12 +1040,6 @@ Rectangle {
10251040 }
10261041 }
10271042
1028− function waitUntilApplicationWindowIsFullyVisible() {
1029− var appDelegate = findChild(shell, "appDelegate0")
1030− var surfaceContainer = findChild(appDelegate, "surfaceContainer");
1031− tryCompareFunction(function() { return surfaceContainer.surface !== null; }, true);
1032− }
1033−
10341043 function waitUntilDashIsFocused() {
10351044 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
10361045 }
@@ -1087,9 +1096,9 @@ Rectangle {
10871096 swipeAwayGreeter();
10881097 var greeter = findChild(shell, "greeter");
10891098
1099+ var appSurfaceId = topLevelSurfaceList.nextId;
10901100 var app = ApplicationManager.startApplication("dialer-app");
1091− // wait until the app is fully loaded (ie, real surface replaces splash screen)
1092− tryCompareFunction(function() { return app.session !== null && app.session.lastSurface !== null }, true);
1101+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
10931102
10941103 // Minimize the application we just launched
10951104 swipeFromLeftEdge(shell.width * 0.75);
@@ -1150,13 +1159,17 @@ Rectangle {
11501159 swipeAwayGreeter();
11511160 var panel = findChild(shell, "panel");
11521161 compare(panel.fullscreenMode, false);
1153− ApplicationManager.startApplication("camera-app");
1162+ var cameraSurfaceId = topLevelSurfaceList.nextId;
1163+ var cameraApp = ApplicationManager.startApplication("camera-app");
1164+ waitUntilAppWindowIsFullyLoaded(cameraSurfaceId);
11541165 tryCompare(panel, "fullscreenMode", true);
1155− ApplicationManager.startApplication("dialer-app");
1166+ var dialerSurfaceId = topLevelSurfaceList.nextId;
1167+ var dialerApp = ApplicationManager.startApplication("dialer-app");
1168+ waitUntilAppWindowIsFullyLoaded(dialerSurfaceId);
11561169 tryCompare(panel, "fullscreenMode", false);
1157− ApplicationManager.requestFocusApplication("camera-app");
1170+ ApplicationManager.requestFocusApplication(cameraApp.appId);
11581171 tryCompare(panel, "fullscreenMode", true);
1159− ApplicationManager.requestFocusApplication("dialer-app");
1172+ ApplicationManager.requestFocusApplication(dialerApp.appId);
11601173 tryCompare(panel, "fullscreenMode", false);
11611174 }
11621175
@@ -1227,24 +1240,32 @@ Rectangle {
12271240 var tutorial = findChild(shell, "tutorial");
12281241 tryCompare(wizard, "active", true);
12291242 tryCompare(tutorial, "running", true);
1230− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1243+ tryCompareFunction(function() { return topLevelSurfaceList.applicationAt(0).appId; }, "unity8-dash");
12311244
1232− // Make sure we stay running when nothing focused (can happen for
1245+ // Make sure we stay running when there's no top level window (can happen for
12331246 // a moment when we restart the dash after switching language)
1234− ApplicationManager.stopApplication("unity8-dash");
1235− tryCompare(ApplicationManager, "focusedApplicationId", "");
1247+ var dashApplication = ApplicationManager.findApplication("unity8-dash");
1248+ ApplicationManager.stopApplication(dashApplication.appId);
1249+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
1250+ // They won't be gone until those surface items are destroyed.
1251+ tryCompareFunction(function() { return dashApplication.surfaceList.count }, 0);
1252+
1253+ tryCompare(topLevelSurfaceList, "count", 0);
12361254 compare(wizard.shown, true);
12371255 compare(tutorial.running, true);
12381256
1239− // And make sure we stay running when dash focused again
1240− ApplicationManager.startApplication("unity8-dash");
1241− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1257+ // And make sure we stay running when dash comes back again
1258+ var dashSurfaceId = topLevelSurfaceList.nextId;
1259+ ApplicationManager.startApplication(dashApplication.appId);
1260+ waitUntilAppWindowIsFullyLoaded(dashSurfaceId);
12421261 compare(wizard.shown, true);
12431262 compare(tutorial.running, true);
12441263
1245− // And make sure we stop when something else is focused
1246− ApplicationManager.startApplication("gallery-app");
1247− tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
1264+ // And make sure we stop when some other surface shows app
1265+ var gallerySurfaceId = topLevelSurfaceList.nextId;
1266+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1267+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
1268+ tryCompareFunction(function() { return topLevelSurfaceList.applicationAt(0).appId; }, "gallery-app");
12481269 compare(wizard.shown, false);
12491270 compare(tutorial.running, false);
12501271 tryCompare(AccountsService, "demoEdges", false);
@@ -1269,7 +1290,7 @@ Rectangle {
12691290 function test_tapOnRightEdgeReachesApplicationSurface() {
12701291 loadShell("phone");
12711292 swipeAwayGreeter();
1272− var topmostSpreadDelegate = findChild(shell, "appDelegate0");
1293+ var topmostSpreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
12731294 verify(topmostSpreadDelegate);
12741295
12751296 waitUntilFocusedApplicationIsShowingItsSurface();
@@ -1298,7 +1319,7 @@ Rectangle {
12981319 function test_rightEdgeDragDoesNotReachApplicationSurface() {
12991320 loadShell("phone");
13001321 swipeAwayGreeter();
1301− var topmostSpreadDelegate = findChild(shell, "appDelegate0");
1322+ var topmostSpreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
13021323 var topmostSurfaceItem = findChild(topmostSpreadDelegate, "surfaceItem");
13031324 var rightEdgeDragArea = findChild(shell, "spreadDragArea");
13041325
@@ -1318,7 +1339,7 @@ Rectangle {
13181339
13191340 function waitUntilFocusedApplicationIsShowingItsSurface()
13201341 {
1321− var spreadDelegate = findChild(shell, "appDelegate0");
1342+ var spreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
13221343 var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
13231344 tryCompare(appState, "state", "surface");
13241345 var transitions = appState.transitions;
@@ -1467,11 +1488,13 @@ Rectangle {
14671488 loadShell("tablet");
14681489 shell.usageScenario = "desktop";
14691490
1491+ var webBrowserSurfaceId = topLevelSurfaceList.nextId;
14701492 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1471− waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1493+ waitUntilAppWindowIsFullyLoaded(webBrowserSurfaceId);
14721494
1495+ var gallerySurfaceId = topLevelSurfaceList.nextId;
14731496 var galleryApp = ApplicationManager.startApplication("gallery-app");
1474− waitUntilAppWindowIsFullyLoaded(galleryApp);
1497+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
14751498
14761499 ApplicationManager.requestFocusApplication("unity8-dash");
14771500 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
@@ -1489,11 +1512,13 @@ Rectangle {
14891512 loadShell("tablet");
14901513 shell.usageScenario = "tablet";
14911514
1515+ var webBrowserSurfaceId = topLevelSurfaceList.nextId;
14921516 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1493− waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1517+ waitUntilAppWindowIsFullyLoaded(webBrowserSurfaceId);
14941518
1519+ var gallerySurfaceId = topLevelSurfaceList.nextId;
14951520 var galleryApp = ApplicationManager.startApplication("gallery-app");
1496− waitUntilAppWindowIsFullyLoaded(galleryApp);
1521+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
14971522
14981523 ApplicationManager.requestFocusApplication("unity8-dash");
14991524 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
@@ -1523,17 +1548,26 @@ Rectangle {
15231548 var desktopStage = findChild(shell, "stage");
15241549 verify(desktopStage != null)
15251550
1551+ var app1SurfaceId = topLevelSurfaceList.nextId;
15261552 var app1 = ApplicationManager.startApplication("dialer-app")
1527− waitUntilAppWindowIsFullyLoaded(app1);
1553+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
1554+
1555+ var app2SurfaceId = topLevelSurfaceList.nextId;
15281556 var app2 = ApplicationManager.startApplication("webbrowser-app")
1529− waitUntilAppWindowIsFullyLoaded(app2);
1557+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
1558+ var app2Surface = app2.surfaceList.get(0);
1559+ verify(app2Surface);
1560+
1561+ var app3SurfaceId = topLevelSurfaceList.nextId;
15301562 var app3 = ApplicationManager.startApplication("camera-app")
1531− waitUntilAppWindowIsFullyLoaded(app3);
1563+ waitUntilAppWindowIsFullyLoaded(app3SurfaceId);
1564+ var app3Surface = app3.surfaceList.get(0);
1565+ verify(app3Surface);
15321566
15331567 // Do a quick alt-tab and see if focus changes
1534− tryCompare(app3.session.lastSurface, "activeFocus", true)
1568+ tryCompare(app3Surface, "activeFocus", true)
15351569 keyClick(Qt.Key_Tab, Qt.AltModifier)
1536− tryCompare(app2.session.lastSurface, "activeFocus", true)
1570+ tryCompare(app2Surface, "activeFocus", true)
15371571
15381572 // Press Alt+Tab
15391573 keyPress(Qt.Key_Alt);
@@ -1541,7 +1575,7 @@ Rectangle {
15411575 keyRelease(Qt.Key_Alt)
15421576
15431577 // Focus should have switched back now
1544− tryCompare(app3.session.lastSurface, "activeFocus", true)
1578+ tryCompare(app3Surface, "activeFocus", true)
15451579 }
15461580
15471581 function test_altTabWrapAround() {
@@ -1590,7 +1624,9 @@ Rectangle {
15901624 tryCompare(desktopSpread, "state", "")
15911625
15921626 // Make sure that after wrapping around once, we have the same one focused as at the beginning
1593− tryCompare(focused.session.lastSurface, "activeFocus", true)
1627+ var focusedAppSurface = focused.surfaceList.get(0);
1628+ verify(focusedAppSurface);
1629+ tryCompare(focusedAppSurface, "activeFocus", true)
15941630 }
15951631
15961632 function test_altBackTabNavigation() {
@@ -1624,7 +1660,7 @@ Rectangle {
16241660 keyRelease(Qt.Key_Alt);
16251661 }
16261662
1627− function test_highlightFollowsMouse() {
1663+ function otest_highlightFollowsMouse() {
16281664 loadDesktopShellWithApps()
16291665
16301666 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
@@ -1661,9 +1697,7 @@ Rectangle {
16611697 keyPress(Qt.Key_Alt)
16621698 keyClick(Qt.Key_Tab);
16631699
1664− appRemovedSpy.clear();
1665−
1666− var closedAppId = ApplicationManager.get(2).appId;
1700+ var surfaceId = topLevelSurfaceList.idAt(2);
16671701 var spreadDelegate2 = spreadRepeater.itemAt(2);
16681702 var closeMouseArea = findChild(spreadDelegate2, "closeMouseArea");
16691703
@@ -1678,12 +1712,14 @@ Rectangle {
16781712 }
16791713 tryCompare(closeMouseArea, "enabled", true)
16801714
1715+ var countBeforeClickingCloseButton = topLevelSurfaceList.count;
1716+ verify(topLevelSurfaceList.indexForId(surfaceId) === 2);
1717+
16811718 // Close the app using the close button
16821719 mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
16831720
1684− // Verify applicationRemoved has been emitted correctly
1685− tryCompare(appRemovedSpy, "count", 1)
1686− compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1721+ tryCompare(topLevelSurfaceList, "count", countBeforeClickingCloseButton - 1);
1722+ verify(topLevelSurfaceList.indexForId(surfaceId) === -1);
16871723
16881724 keyRelease(Qt.Key_Alt);
16891725 }
@@ -1708,7 +1744,7 @@ Rectangle {
17081744 keyPress(Qt.Key_Alt)
17091745 keyClick(Qt.Key_Tab);
17101746
1711− var focusAppId = ApplicationManager.get(2).appId;
1747+ var surface = topLevelSurfaceList.surfaceAt(2);
17121748 var spreadDelegate2 = spreadRepeater.itemAt(2);
17131749 var clippedSpreadDelegate = findChild(spreadDelegate2, "clippedSpreadDelegate");
17141750
@@ -1730,7 +1766,7 @@ Rectangle {
17301766
17311767 // Verify that we left the spread and app2 is the focused one now
17321768 tryCompare(stage, "state", "");
1733− tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1769+ tryCompare(surface, "focused", true);
17341770
17351771 keyRelease(Qt.Key_Alt);
17361772 }
@@ -1794,7 +1830,7 @@ Rectangle {
17941830 function test_workspacePreviewsHighlightedApp() {
17951831 loadDesktopShellWithApps()
17961832
1797− var targetZ = ApplicationManager.count + 1;
1833+ var targetZ = topLevelSurfaceList.count + 1;
17981834
17991835 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
18001836 verify(spreadRepeater !== null);
@@ -1870,7 +1906,8 @@ Rectangle {
18701906 loadDesktopShellWithApps();
18711907
18721908 var appRepeater = findChild(shell, "appRepeater")
1873− var appId = ApplicationManager.get(0).appId;
1909+ var application = topLevelSurfaceList.applicationAt(0);
1910+ var surfaceId = topLevelSurfaceList.idAt(0);
18741911 var appDelegate = appRepeater.itemAt(0);
18751912 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
18761913
@@ -1881,17 +1918,20 @@ Rectangle {
18811918 tryCompare(appDelegate, "state", "maximized");
18821919 tryCompare(PanelState, "buttonsVisible", true)
18831920
1884− ApplicationManager.stopApplication(appId);
1921+ ApplicationManager.stopApplication(application.appId);
18851922 tryCompare(PanelState, "buttonsVisible", false)
18861923
1887− ApplicationManager.startApplication(appId);
1924+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
1925+ // They won't be gone until those surface items are destroyed.
1926+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
1927+
1928+ ApplicationManager.startApplication(application.appId);
18881929 tryCompare(PanelState, "buttonsVisible", true)
18891930 }
18901931
18911932 function test_newAppHasValidGeometry() {
18921933 loadDesktopShellWithApps();
18931934 var appRepeater = findChild(shell, "appRepeater");
1894− var appId = ApplicationManager.get(0).appId;
18951935 var appDelegate = appRepeater.itemAt(0);
18961936
18971937 var resizeArea = findChild(appDelegate, "windowResizeArea");
@@ -1908,7 +1948,6 @@ Rectangle {
19081948 function test_switchToStagedHidesPanelButtons() {
19091949 loadDesktopShellWithApps();
19101950 var appRepeater = findChild(shell, "appRepeater")
1911− var appId = ApplicationManager.get(0).appId;
19121951 var appDelegate = appRepeater.itemAt(0);
19131952 var panelButtons = findChild(shell, "panelWindowControlButtons")
19141953
@@ -1925,7 +1964,6 @@ Rectangle {
19251964 function test_lockingGreeterHidesPanelButtons() {
19261965 loadDesktopShellWithApps();
19271966 var appRepeater = findChild(shell, "appRepeater")
1928− var appId = ApplicationManager.get(0).appId;
19291967 var appDelegate = appRepeater.itemAt(0);
19301968 var panelButtons = findChild(shell, "panelWindowControlButtons")
19311969
@@ -1955,12 +1993,15 @@ Rectangle {
19551993 shell.usageScenario = "desktop";
19561994 waitForRendering(shell);
19571995
1996+ var appSurfaceId = topLevelSurfaceList.nextId;
19581997 var app = ApplicationManager.startApplication("dialer-app")
1959− waitUntilAppWindowIsFullyLoaded(app);
1998+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
19601999
19612000 var appContainer = findChild(shell, "appContainer");
1962− var appDelegate = findChild(appContainer, "appDelegate_dialer-app");
1963− var decoration = findChild(appDelegate, "appWindowDecoration_dialer-app");
2001+ verify(appContainer);
2002+ var appDelegate = findChild(appContainer, "appDelegate_" + appSurfaceId);
2003+ verify(appDelegate);
2004+ var decoration = findChild(appDelegate, "appWindowDecoration");
19642005 verify(decoration);
19652006
19662007 // move it away from launcher and panel
@@ -1978,18 +2019,22 @@ Rectangle {
19782019 function test_restoreWindowStateFixesIfUnderPanel() {
19792020 loadDesktopShellWithApps();
19802021 var appRepeater = findChild(shell, "appRepeater")
1981− var appId = ApplicationManager.get(0).appId;
2022+ var application = topLevelSurfaceList.applicationAt(0);
19822023 var appDelegate = appRepeater.itemAt(0);
19832024
19842025 // Move it under the panel programmatically (might happen later with an alt+drag)
19852026 appDelegate.y = -units.gu(10)
19862027
1987− ApplicationManager.stopApplication(appId)
1988− ApplicationManager.startApplication(appId)
2028+ ApplicationManager.stopApplication(application.appId);
2029+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
2030+ // They won't be gone until those surface items are destroyed.
2031+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
2032+
2033+ ApplicationManager.startApplication(application.appId);
19892034 waitForRendering(shell)
19902035
19912036 // Make sure the newly started one is at index 0 again
1992− tryCompare(ApplicationManager.get(0), "appId", appId);
2037+ tryCompareFunction(function () { return topLevelSurfaceList.applicationAt(0).appId; }, application.appId);
19932038
19942039 appDelegate = appRepeater.itemAt(0);
19952040 compare(appDelegate.y >= PanelState.panelHeight, true);
@@ -2008,16 +2053,22 @@ Rectangle {
20082053
20092054 // Add two main stage apps, the second in order to suspend the first.
20102055 // LibreOffice has isTouchApp set to false by our mocks.
2056+ var app1SurfaceId = topLevelSurfaceList.nextId;
20112057 var app1 = ApplicationManager.startApplication("libreoffice");
2012− waitUntilAppWindowIsFullyLoaded(app1);
2058+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
2059+ var app2SurfaceId = topLevelSurfaceList.nextId;
20132060 var app2 = ApplicationManager.startApplication("gallery-app");
2014− waitUntilAppWindowIsFullyLoaded(app2);
2061+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
20152062
20162063 // Sanity checking
20172064 compare(app1.stage, ApplicationInfoInterface.MainStage);
20182065 compare(app2.stage, ApplicationInfoInterface.MainStage);
20192066 verify(!app1.isTouchApp);
2020− verify(!app1.session.lastSurface.activeFocus);
2067+
2068+ var app1Surface = app1.surfaceList.get(0);
2069+ verify(app1Surface);
2070+
2071+ verify(!app1Surface.activeFocus);
20212072
20222073 // Make sure app1 is exempt with a requested suspend
20232074 verify(app1.exemptFromLifecycle);
@@ -2038,15 +2089,21 @@ Rectangle {
20382089 GSettingsController.setLifecycleExemptAppids(["webbrowser-app"]);
20392090
20402091 // Add two main stage apps, the second in order to suspend the first
2092+ var app1SurfaceId = topLevelSurfaceList.nextId;
20412093 var app1 = ApplicationManager.startApplication("webbrowser-app");
2042− waitUntilAppWindowIsFullyLoaded(app1);
2094+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
2095+ var app2SurfaceId = topLevelSurfaceList.nextId;
20432096 var app2 = ApplicationManager.startApplication("gallery-app");
2044− waitUntilAppWindowIsFullyLoaded(app2);
2097+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
20452098
20462099 // Sanity checking
20472100 compare(app1.stage, ApplicationInfoInterface.MainStage);
20482101 compare(app2.stage, ApplicationInfoInterface.MainStage);
2049− verify(!app1.session.lastSurface.activeFocus);
2102+
2103+ var app1Surface = app1.surfaceList.get(0);
2104+ verify(app1Surface);
2105+
2106+ verify(!app1Surface.activeFocus);
20502107
20512108 // Make sure app1 is exempt with a requested suspend
20522109 verify(app1.exemptFromLifecycle);
@@ -2205,9 +2262,10 @@ Rectangle {
22052262 var appContainer = findChild(shell, "appContainer");
22062263 var launcher = findChild(shell, "launcher");
22072264
2265+ var appSurfaceId = topLevelSurfaceList.nextId;
22082266 var app = ApplicationManager.startApplication("music-app");
2209− waitUntilAppWindowIsFullyLoaded(app);
2210− var appDelegate = findChild(appContainer, "appDelegate_music-app");
2267+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
2268+ var appDelegate = findChild(appContainer, "appDelegate_" + appSurfaceId);
22112269 appDelegate.maximize();
22122270 tryCompare(appDelegate, "visuallyMaximized", true);
22132271 waitForRendering(shell);
@@ -2235,8 +2293,18 @@ Rectangle {
22352293
22362294 tryCompare(launcher, "lockedVisible", true);
22372295
2238− var cameraApp = ApplicationManager.startApplication("camera-app");
2239− waitUntilAppWindowIsFullyLoaded(cameraApp);
2296+ var surfaceId = topLevelSurfaceList.nextId;
2297+ var app = ApplicationManager.startApplication("gmail-webapp");
2298+
2299+ waitUntilAppWindowIsFullyLoaded(surfaceId);
2300+
2301+ // Sanity check: ensure the fake app we chose creates a surface the way
2302+ // we expect it to.
2303+ compare(app.surfaceList.get(0).shellChrome, Mir.NormalChrome);
2304+
2305+ compare(launcher.lockedVisible, true);
2306+
2307+ app.surfaceList.get(0).state = Mir.FullscreenState;
22402308
22412309 tryCompare(launcher, "lockedVisible", false);
22422310 }
@@ -2257,8 +2325,9 @@ Rectangle {
22572325 swipeAwayGreeter();
22582326
22592327 // Let's open a fullscreen app
2328+ var appSurfaceId = topLevelSurfaceList.nextId;
22602329 var app = ApplicationManager.startApplication("camera-app");
2261− waitUntilAppWindowIsFullyLoaded(app);
2330+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
22622331
22632332 var appRepeater = findChild(shell, data.repeaterName);
22642333 var topmostAppDelegate = appRepeater.itemAt(0);
@@ -2295,19 +2364,21 @@ Rectangle {
22952364 AccountsService.keymaps = ["sk", "cz+qwerty", "fr"] // "configure" the keymaps for user
22962365
22972366 // start some app
2367+ var appSurfaceId = topLevelSurfaceList.nextId;
22982368 var app = ApplicationManager.startApplication("dialer-app");
2299− waitUntilAppWindowIsFullyLoaded(app);
2369+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
2370+ var appSurface = app.surfaceList.get(0);
23002371
23012372 // verify the initial keymap of the newly started app is the first one from the list
2302− tryCompare(app.session.lastSurface, "keymap", "sk");
2373+ tryCompare(appSurface, "keymap", "sk");
23032374
23042375 // switch to next keymap, should go to "cz+qwerty"
23052376 keyClick(Qt.Key_Space, Qt.MetaModifier);
2306− tryCompare(app.session.lastSurface, "keymap", "cz+qwerty");
2377+ tryCompare(appSurface, "keymap", "cz+qwerty");
23072378
23082379 // switch to next keymap, should go to "fr"
23092380 keyClick(Qt.Key_Space, Qt.MetaModifier);
2310− tryCompare(app.session.lastSurface, "keymap", "fr");
2381+ tryCompare(appSurface, "keymap", "fr");
23112382
23122383 // go to e.g. desktop stage
23132384 loadShell("desktop");
@@ -2315,30 +2386,32 @@ Rectangle {
23152386 waitForRendering(shell);
23162387
23172388 // start a second app, should get the last configured keyboard, "fr"
2389+ var app2SurfaceId = topLevelSurfaceList.nextId;
23182390 var app2 = ApplicationManager.startApplication("calendar-app");
2319− waitUntilAppWindowIsFullyLoaded(app2);
2320− tryCompare(app2.session.lastSurface, "keymap", "fr");
2391+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
2392+ var app2Surface = app2.surfaceList.get(0);
2393+ tryCompare(app2Surface, "keymap", "fr");
23212394
23222395 // focus our first app, make sure it also has the "fr" keymap
23232396 ApplicationManager.requestFocusApplication("dialer-app");
2324− tryCompare(app.session.lastSurface, "keymap", "fr");
2397+ tryCompare(appSurface, "keymap", "fr");
23252398
23262399 // switch to previous keymap, should be "cz+qwerty"
23272400 keyClick(Qt.Key_Space, Qt.MetaModifier|Qt.ShiftModifier);
2328− tryCompare(app.session.lastSurface, "keymap", "cz+qwerty");
2401+ tryCompare(appSurface, "keymap", "cz+qwerty");
23292402
23302403 // go next twice to "sk", past the end
23312404 keyClick(Qt.Key_Space, Qt.MetaModifier);
23322405 keyClick(Qt.Key_Space, Qt.MetaModifier);
2333− tryCompare(app.session.lastSurface, "keymap", "sk");
2406+ tryCompare(appSurface, "keymap", "sk");
23342407
23352408 // go back once to past the beginning, to "fr"
23362409 keyClick(Qt.Key_Space, Qt.MetaModifier|Qt.ShiftModifier);
2337− tryCompare(app.session.lastSurface, "keymap", "fr");
2410+ tryCompare(appSurface, "keymap", "fr");
23382411
23392412 // switch to app2, should also get "fr"
23402413 ApplicationManager.requestFocusApplication("calendar-app");
2341− tryCompare(app2.session.lastSurface, "keymap", "fr");
2414+ tryCompare(app2Surface, "keymap", "fr");
23422415 }
23432416 }
23442417}
tests/qmltests/tst_ShellWithPin.qml+9−13
@@ -197,14 +197,6 @@ Item {
197197 removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
198198 }
199199
200− function killApps() {
201− while (ApplicationManager.count > 1) {
202− var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
203− ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
204− }
205− compare(ApplicationManager.count, 1)
206− }
207−
208200 function swipeAwayGreeter(waitForCoverPage) {
209201 var greeter = findChild(shell, "greeter");
210202 waitForRendering(greeter)
@@ -318,6 +310,7 @@ Item {
318310 var emergencyButton = findChild(greeter, "emergencyCallLabel");
319311 tap(emergencyButton)
320312
313+
321314 tryCompare(greeter, "shown", false);
322315 killApps() // kill dialer-app, as if it crashed
323316 tryCompare(greeter, "shown", true);
@@ -424,12 +417,15 @@ Item {
424417
425418 var greeter = findChild(shell, "greeter");
426419 var lockscreen = findChild(shell, "lockscreen");
420+ verify(lockscreen);
427421
428422 lockscreen.emergencyCall();
429423 confirmLockedApp("dialer-app");
430424 callManager.foregroundCall = phoneCall;
431425
432426 LightDM.Greeter.showGreeter();
427+ lockscreen = findChild(shell, "lockscreen");
428+ verify(lockscreen);
433429 tryCompare(lockscreen, "shown", true);
434430 tryCompare(greeter, "hasLockedApp", false);
435431
@@ -476,7 +472,7 @@ Item {
476472
477473 var app = ApplicationManager.startApplication("gallery-app");
478474 // wait until the app is fully loaded (ie, real surface replaces splash screen)
479− tryCompareFunction(function() { return app.session !== null && app.session.lastSurface !== null }, true);
475+ tryCompareFunction(function() { return app.session !== null && app.surfaceList.count > 0 }, true);
480476
481477 // New app hides coverPage?
482478 var greeter = findChild(shell, "greeter");
@@ -593,8 +589,8 @@ Item {
593589 var coverPage = findChild(shell, "coverPage");
594590 var lockscreen = findChild(shell, "lockscreen");
595591 var launcher = findChild(shell, "launcherPanel");
596− ApplicationManager.startApplication("gallery-app");
597− tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
592+ var galleryApp = ApplicationManager.startApplication("gallery-app");
593+ tryCompare(shell, "mainApp", galleryApp);
598594
599595 // Show greeter
600596 LightDM.Greeter.showGreeter();
@@ -605,13 +601,13 @@ Item {
605601 tryCompare(launcher, "x", -launcher.width);
606602 tryCompare(coverPage, "showProgress", 0);
607603 compare(lockscreen.shown, true);
608− compare(ApplicationManager.focusedApplicationId, "gallery-app");
604+ tryCompare(shell, "mainApp", galleryApp);
609605
610606 // Now attempt a swipe on lockscreen
611607 touchFlick(shell, 2, shell.height / 2, units.gu(30), shell.height / 2);
612608 tryCompare(launcher, "x", 0);
613609 compare(lockscreen.shown, true);
614− compare(ApplicationManager.focusedApplicationId, "gallery-app");
610+ tryCompare(shell, "mainApp", galleryApp);
615611 }
616612 }
617613}
tests/utils/modules/Unity/Test/UnityTestCase.qml+20−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013-2015 Canonical Ltd.
2+ * Copyright 2013-2016 Canonical Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -16,12 +16,14 @@
1616
1717import QtQuick 2.4
1818import QtTest 1.0
19+import Unity.Application 0.1
1920import Ubuntu.Components 1.3
2021import Unity.Test 0.1 as UT
22+import Utils 0.1
2123
2224TestCase {
2325 id: testCase
24− TestUtil {id:util}
26+ property var util: TestUtil {id:util}
2527
2628 // This is needed for waitForRendering calls to return
2729 // if the watched element already got rendered
@@ -538,4 +540,20 @@ TestCase {
538540 tryCompare(transition, "running", false, 2000);
539541 }
540542 }
543+
544+ /*
545+ kill all (fake) running apps but unity8-dash, bringing Unity.Application back to its initial state
546+ */
547+ function killApps() {
548+ while (ApplicationManager.count > 1) {
549+ var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
550+ var application = ApplicationManager.get(appIndex);
551+ ApplicationManager.stopApplication(application.appId);
552+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
553+ // They won't be gone until those surface items are destroyed.
554+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
555+ tryCompare(application, "state", ApplicationInfo.Stopped);
556+ }
557+ compare(ApplicationManager.count, 1);
558+ }
541559}