on xenial help sign in
Surface-based window management
commit20b32fcd6864b0ca81b0abe8ce06130bfaa54fe5 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateMon 4 Apr 2016, 10:37 (11 years ago) parent7a0b765
65 files changed+3791−2853
plugins/CMakeLists.txt+1 plugins/WindowManager/CMakeLists.txt+13 plugins/WindowManager/TopLevelSurfaceList.cpp+429 plugins/WindowManager/TopLevelSurfaceList.h+211 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+2−9 qml/Greeter/Greeter.qml+5−4 qml/Launcher/LauncherPanel.qml+1 qml/Shell.qml+64−51 qml/Stages/AbstractStage.qml+9−2 qml/Stages/Animations/BaseSessionAnimation.qml−96 qml/Stages/Animations/SwipeFromBottomAnimation.qml−53 qml/Stages/ApplicationWindow.qml+106−68 qml/Stages/DecoratedWindow.qml+4−4 qml/Stages/DesktopSpread.qml+20−18 qml/Stages/DesktopSpreadDelegate.qml+4−3 qml/Stages/DesktopStage.qml+110−82 qml/Stages/PhoneStage.qml+124−84 qml/Stages/PromptSurfaceAnimations.qml+77 qml/Stages/SessionContainer.qml−180 qml/Stages/SpreadDelegate.qml+4−3 qml/Stages/SurfaceContainer.qml+97−40 qml/Stages/TabletStage.qml+281−223 qml/Stages/TransformedSpreadDelegate.qml+4−4 qml/Stages/TransformedTabletSpreadDelegate.qml+29−23 qml/Stages/WindowedFullscreenPolicy.qml+5−7 tests/mocks/Unity/Application/ApplicationInfo.cpp+168−110 tests/mocks/Unity/Application/ApplicationInfo.h+40−24 tests/mocks/Unity/Application/ApplicationManager.cpp+104−40 tests/mocks/Unity/Application/ApplicationManager.h+12−8 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+128−7 tests/mocks/Unity/Application/MirSurface.h+49−5 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+5−4 tests/mocks/Unity/Application/SurfaceManager.h+2 tests/mocks/Unity/Application/plugin.cpp+5−27 tests/mocks/Utils/CMakeLists.txt+4 tests/mocks/Utils/plugin.cpp+1−1 tests/plugins/Unity/Launcher/launchermodeltest.cpp+1 tests/qmltests/CMakeLists.txt−1 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+228−159 tests/qmltests/Stages/tst_PhoneStage.qml+255−82 tests/qmltests/Stages/tst_SessionContainer.qml−331 tests/qmltests/Stages/tst_SurfaceContainer.qml+230−46 tests/qmltests/Stages/tst_TabletStage.qml+250−84 tests/qmltests/Tutorial/tst_Tutorial.qml−8 tests/qmltests/tst_OrientedShell.qml+76−55 tests/qmltests/tst_Shell.qml+182−116 tests/qmltests/tst_ShellWithPin.qml+9−13 tests/utils/modules/Unity/Test/UnityTestCase.qml+20−2
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+429
@@ -0,0 +1,429 @@
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+Q_LOGGING_CATEGORY(UNITY_TOPSURFACELIST, "unity.topsurfacelist")
25+
26+#define DEBUG_MSG qCDebug(UNITY_TOPSURFACELIST).nospace().noquote() << __func__
27+
28+namespace unityapi = unity::shell::application;
29+
30+TopLevelSurfaceList::TopLevelSurfaceList(QObject *parent) :
31+ QAbstractListModel(parent)
32+{
33+ DEBUG_MSG << "()";
34+}
35+
36+TopLevelSurfaceList::~TopLevelSurfaceList()
37+{
38+ DEBUG_MSG << "()";
39+}
40+
41+int TopLevelSurfaceList::rowCount(const QModelIndex &parent) const
42+{
43+ return !parent.isValid() ? m_surfaceList.size() : 0;
44+}
45+
46+QVariant TopLevelSurfaceList::data(const QModelIndex& index, int role) const
47+{
48+ if (index.row() < 0 || index.row() >= m_surfaceList.size())
49+ return QVariant();
50+
51+ if (role == SurfaceRole) {
52+ unityapi::MirSurfaceInterface *surface = m_surfaceList.at(index.row()).surface;
53+ return QVariant::fromValue(surface);
54+ } else if (role == ApplicationRole) {
55+ return QVariant::fromValue(m_surfaceList.at(index.row()).application);
56+ } else if (role == IdRole) {
57+ return QVariant::fromValue(m_surfaceList.at(index.row()).id);
58+ } else {
59+ return QVariant();
60+ }
61+}
62+
63+void TopLevelSurfaceList::raise(unityapi::MirSurfaceInterface *surface)
64+{
65+ if (!surface)
66+ return;
67+
68+ DEBUG_MSG << "(MirSurface[" << (void*)surface << "])";
69+
70+ int i = indexOf(surface);
71+ if (i != -1) {
72+ moveSurface(i, 0);
73+ }
74+}
75+
76+void TopLevelSurfaceList::prependPlaceholder(unityapi::ApplicationInfoInterface *application)
77+{
78+ DEBUG_MSG << "(" << application->appId() << ")";
79+ prependSurfaceHelper(nullptr, application);
80+}
81+
82+void TopLevelSurfaceList::prependSurface(unityapi::MirSurfaceInterface *surface, unityapi::ApplicationInfoInterface *application)
83+{
84+ Q_ASSERT(surface != nullptr);
85+
86+ bool filledPlaceholder = false;
87+ for (int i = 0; i < m_surfaceList.count() && !filledPlaceholder; ++i) {
88+ ModelEntry &entry = m_surfaceList[i];
89+ if (entry.application == application && entry.surface == nullptr) {
90+ entry.surface = surface;
91+ connectSurface(surface);
92+ DEBUG_MSG << " appId=" << application->appId() << " surface=" << surface << ", filling out placeholder. after: " << toString();
93+ Q_EMIT dataChanged(index(i) /* topLeft */, index(i) /* bottomRight */, QVector<int>() << SurfaceRole);
94+ filledPlaceholder = true;
95+ }
96+ }
97+
98+ if (!filledPlaceholder) {
99+ DEBUG_MSG << " appId=" << application->appId() << " surface=" << surface << ", adding new row";
100+ prependSurfaceHelper(surface, application);
101+ }
102+}
103+
104+void TopLevelSurfaceList::prependSurfaceHelper(unityapi::MirSurfaceInterface *surface, unityapi::ApplicationInfoInterface *application)
105+{
106+ beginInsertRows(QModelIndex(), 0 /*first*/, 0 /*last*/);
107+ int id = generateId();
108+ m_surfaceList.prepend(ModelEntry(surface, application, id));
109+ if (surface) {
110+ connectSurface(surface);
111+ }
112+ endInsertRows();
113+ DEBUG_MSG << " after " << toString();
114+ Q_EMIT countChanged();
115+ Q_EMIT listChanged();
116+}
117+
118+void TopLevelSurfaceList::connectSurface(unityapi::MirSurfaceInterface *surface)
119+{
120+ connect(surface, &unityapi::MirSurfaceInterface::focusedChanged, this, [this, surface](bool focused){
121+ if (focused) {
122+ this->raise(surface);
123+ }
124+ });
125+ connect(surface, &unityapi::MirSurfaceInterface::liveChanged, this, [this, surface](bool live){
126+ if (!live) {
127+ onSurfaceDied(surface);
128+ }
129+ });
130+ connect(surface, &QObject::destroyed, this, [this, surface](){ this->onSurfaceDestroyed(surface); });
131+}
132+
133+void TopLevelSurfaceList::onSurfaceDied(unityapi::MirSurfaceInterface *surface)
134+{
135+ int i = indexOf(surface);
136+ if (i == -1) {
137+ return;
138+ }
139+
140+ auto application = m_surfaceList[i].application;
141+
142+ // can't be starting if it already has a surface
143+ Q_ASSERT(application->state() != unityapi::ApplicationInfoInterface::Starting);
144+
145+ if (application->state() == unityapi::ApplicationInfoInterface::Running) {
146+ m_surfaceList[i].removeOnceSurfaceDestroyed = true;
147+ } else {
148+ // assume it got killed by the out-of-memory daemon.
149+ //
150+ // So leave entry in the model and only remove its surface, so shell can display a screenshot
151+ // in its place.
152+ m_surfaceList[i].removeOnceSurfaceDestroyed = false;
153+ }
154+}
155+
156+void TopLevelSurfaceList::onSurfaceDestroyed(unityapi::MirSurfaceInterface *surface)
157+{
158+ int i = indexOf(surface);
159+ if (i == -1) {
160+ return;
161+ }
162+
163+ if (m_surfaceList[i].removeOnceSurfaceDestroyed) {
164+ removeAt(i);
165+ } else {
166+ m_surfaceList[i].surface = nullptr;
167+ Q_EMIT dataChanged(index(i) /* topLeft */, index(i) /* bottomRight */, QVector<int>() << SurfaceRole);
168+ DEBUG_MSG << " Removed surface from entry. After: " << toString();
169+ }
170+}
171+
172+void TopLevelSurfaceList::removeAt(int index)
173+{
174+ beginRemoveRows(QModelIndex(), index, index);
175+ m_surfaceList.removeAt(index);
176+ endRemoveRows();
177+ DEBUG_MSG << " after " << toString();
178+ Q_EMIT countChanged();
179+ Q_EMIT listChanged();
180+}
181+
182+int TopLevelSurfaceList::indexOf(unityapi::MirSurfaceInterface *surface)
183+{
184+ for (int i = 0; i < m_surfaceList.count(); ++i) {
185+ if (m_surfaceList.at(i).surface == surface) {
186+ return i;
187+ }
188+ }
189+ return -1;
190+}
191+
192+void TopLevelSurfaceList::moveSurface(int from, int to)
193+{
194+ if (from == to) return;
195+ DEBUG_MSG << " from=" << from << " to=" << to;
196+
197+ if (from >= 0 && from < m_surfaceList.size() && to >= 0 && to < m_surfaceList.size()) {
198+ QModelIndex parent;
199+ /* When moving an item down, the destination index needs to be incremented
200+ by one, as explained in the documentation:
201+ http://qt-project.org/doc/qt-5.0/qtcore/qabstractitemmodel.html#beginMoveRows */
202+
203+ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
204+ m_surfaceList.move(from, to);
205+ DEBUG_MSG << " after " << toString();
206+ endMoveRows();
207+ Q_EMIT listChanged();
208+ }
209+}
210+
211+unityapi::MirSurfaceInterface *TopLevelSurfaceList::surfaceAt(int index) const
212+{
213+ if (index >=0 && index < m_surfaceList.count()) {
214+ return m_surfaceList[index].surface;
215+ } else {
216+ return nullptr;
217+ }
218+}
219+
220+unityapi::ApplicationInfoInterface *TopLevelSurfaceList::applicationAt(int index) const
221+{
222+ if (index >=0 && index < m_surfaceList.count()) {
223+ return m_surfaceList[index].application;
224+ } else {
225+ return nullptr;
226+ }
227+}
228+
229+int TopLevelSurfaceList::idAt(int index) const
230+{
231+ if (index >=0 && index < m_surfaceList.count()) {
232+ return m_surfaceList[index].id;
233+ } else {
234+ return 0;
235+ }
236+}
237+
238+int TopLevelSurfaceList::indexForId(int id) const
239+{
240+ for (int i = 0; i < m_surfaceList.count(); ++i) {
241+ if (m_surfaceList[i].id == id) {
242+ return i;
243+ }
244+ }
245+ return -1;
246+}
247+
248+void TopLevelSurfaceList::move(int id, int toIndex)
249+{
250+ DEBUG_MSG << " id=" << id << " toIndex=" << toIndex;
251+ int fromIndex = indexForId(id);
252+ if (fromIndex != -1) {
253+ moveSurface(fromIndex, toIndex);
254+ }
255+}
256+
257+int TopLevelSurfaceList::generateId()
258+{
259+ int id = m_nextId;
260+ m_nextId = nextFreeId(m_nextId + 1);
261+ Q_EMIT nextIdChanged();
262+ return id;
263+}
264+
265+int TopLevelSurfaceList::nextFreeId(int candidateId)
266+{
267+ if (candidateId > m_maxId) {
268+ return nextFreeId(1);
269+ } else {
270+ if (indexForId(candidateId) == -1) {
271+ // it's indeed free
272+ return candidateId;
273+ } else {
274+ return nextFreeId(candidateId + 1);
275+ }
276+ }
277+}
278+
279+QString TopLevelSurfaceList::toString()
280+{
281+ QString str;
282+ for (int i = 0; i < m_surfaceList.count(); ++i) {
283+ auto item = m_surfaceList.at(i);
284+
285+ QString itemStr = QString("(index=%1,appId=%2,surface=0x%3,id=%4)")
286+ .arg(i)
287+ .arg(item.application->appId())
288+ .arg((qintptr)item.surface, 0, 16)
289+ .arg(item.id);
290+
291+ if (i > 0) {
292+ str.append(",");
293+ }
294+ str.append(itemStr);
295+ }
296+ return str;
297+}
298+
299+void TopLevelSurfaceList::addApplication(unityapi::ApplicationInfoInterface *application)
300+{
301+ DEBUG_MSG << "(" << application->appId() << ")";
302+ Q_ASSERT(!m_applications.contains(application));
303+ m_applications.append(application);
304+
305+ unityapi::MirSurfaceListInterface *surfaceList = application->surfaceList();
306+
307+ if (application->state() != unityapi::ApplicationInfoInterface::Stopped) {
308+ if (surfaceList->count() == 0) {
309+ prependPlaceholder(application);
310+ } else {
311+ for (int i = 0; i < surfaceList->count(); ++i) {
312+ prependSurface(surfaceList->get(i), application);
313+ }
314+ }
315+ }
316+
317+ connect(surfaceList, &QAbstractItemModel::rowsInserted, this,
318+ [this, application, surfaceList](const QModelIndex & /*parent*/, int first, int last)
319+ {
320+ for (int i = last; i >= first; --i) {
321+ this->prependSurface(surfaceList->get(i), application);
322+ }
323+ });
324+}
325+
326+void TopLevelSurfaceList::removeApplication(unityapi::ApplicationInfoInterface *application)
327+{
328+ DEBUG_MSG << "(" << application->appId() << ")";
329+ Q_ASSERT(m_applications.contains(application));
330+
331+ unityapi::MirSurfaceListInterface *surfaceList = application->surfaceList();
332+
333+ disconnect(surfaceList, 0, this, 0);
334+
335+ int i = 0;
336+ while (i < m_surfaceList.count()) {
337+ if (m_surfaceList.at(i).application == application) {
338+ beginRemoveRows(QModelIndex(), i, i);
339+ m_surfaceList.removeAt(i);
340+ endRemoveRows();
341+ Q_EMIT countChanged();
342+ Q_EMIT listChanged();
343+ } else {
344+ ++i;
345+ }
346+ }
347+
348+ DEBUG_MSG << " after " << toString();
349+
350+ m_applications.removeAll(application);
351+}
352+
353+QAbstractListModel *TopLevelSurfaceList::applicationsModel() const
354+{
355+ return m_applicationsModel;
356+}
357+
358+void TopLevelSurfaceList::setApplicationsModel(QAbstractListModel* value)
359+{
360+ if (m_applicationsModel == value) {
361+ return;
362+ }
363+
364+ DEBUG_MSG << "(" << value << ")";
365+
366+ beginResetModel();
367+
368+ if (m_applicationsModel) {
369+ m_surfaceList.clear();
370+ m_applications.clear();
371+ disconnect(m_applicationsModel, 0, this, 0);
372+ }
373+
374+ m_applicationsModel = value;
375+
376+ if (m_applicationsModel) {
377+ findApplicationRole();
378+
379+ connect(m_applicationsModel, &QAbstractItemModel::rowsInserted,
380+ this, [this](const QModelIndex &/*parent*/, int first, int last) {
381+ for (int i = first; i <= last; ++i) {
382+ auto application = getApplicationFromModelAt(i);
383+ addApplication(application);
384+ }
385+ });
386+
387+ connect(m_applicationsModel, &QAbstractItemModel::rowsAboutToBeRemoved,
388+ this, [this](const QModelIndex &/*parent*/, int first, int last) {
389+ for (int i = first; i <= last; ++i) {
390+ auto application = getApplicationFromModelAt(i);
391+ removeApplication(application);
392+ }
393+ });
394+
395+ for (int i = 0; i < m_applicationsModel->rowCount(); ++i) {
396+ auto application = getApplicationFromModelAt(i);
397+ addApplication(application);
398+ }
399+ }
400+
401+ endResetModel();
402+}
403+
404+unityapi::ApplicationInfoInterface *TopLevelSurfaceList::getApplicationFromModelAt(int index)
405+{
406+ QModelIndex modelIndex = m_applicationsModel->index(index);
407+
408+ QVariant variant = m_applicationsModel->data(modelIndex, m_applicationRole);
409+
410+ // variant.value<unityapi::ApplicationInfoInterface*>() returns null for some reason.
411+ return static_cast<unityapi::ApplicationInfoInterface*>(variant.value<QObject*>());
412+}
413+
414+void TopLevelSurfaceList::findApplicationRole()
415+{
416+ QHash<int, QByteArray> namesHash = m_applicationsModel->roleNames();
417+ QHash<int, QByteArray>::iterator i;
418+
419+ m_applicationRole = -1;
420+ for (i = namesHash.begin(); i != namesHash.end() && m_applicationRole == -1; ++i) {
421+ if (i.value() == "application") {
422+ m_applicationRole = i.key();
423+ }
424+ }
425+
426+ if (m_applicationRole == -1) {
427+ qFatal("TopLevelSurfaceList: applicationsModel must have a \"application\" role.");
428+ }
429+}
plugins/WindowManager/TopLevelSurfaceList.hnew file+211
@@ -0,0 +1,211 @@
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 {
95+ QHash<int, QByteArray> roleNames;
96+ roleNames.insert(SurfaceRole, "surface");
97+ roleNames.insert(ApplicationRole, "application");
98+ roleNames.insert(IdRole, "id");
99+ return roleNames;
100+ }
101+
102+ int nextId() const { return m_nextId; }
103+
104+ QAbstractListModel *applicationsModel() const;
105+ void setApplicationsModel(QAbstractListModel*);
106+
107+public Q_SLOTS:
108+ /**
109+ * @brief Returns the surface at the given index
110+ *
111+ * It will be a nullptr if the application is still starting up and thus hasn't yet created
112+ * and drawn into a surface.
113+ */
114+ unity::shell::application::MirSurfaceInterface *surfaceAt(int index) const;
115+
116+ /**
117+ * @brief Returns the application at the given index
118+ */
119+ unity::shell::application::ApplicationInfoInterface *applicationAt(int index) const;
120+
121+ /**
122+ * @brief Returns the unique id of the element at the given index
123+ */
124+ int idAt(int index) const;
125+
126+ /**
127+ * @brief Returns the index where the row with the given id is located
128+ */
129+ int indexForId(int id) const;
130+
131+ /**
132+ * @brief Moves the row with the given id to the specified index
133+ */
134+ void move(int id, int toIndex);
135+
136+Q_SIGNALS:
137+ void countChanged();
138+
139+ /**
140+ * @brief Emitted when the list changes
141+ *
142+ * Emitted when model gains an element, loses an element or when elements exchange positions.
143+ */
144+ void listChanged();
145+
146+ void nextIdChanged();
147+
148+ void applicationsModelChanged();
149+
150+private:
151+ void addApplication(unity::shell::application::ApplicationInfoInterface *application);
152+ void removeApplication(unity::shell::application::ApplicationInfoInterface *application);
153+
154+ int indexOf(unity::shell::application::MirSurfaceInterface *surface);
155+ void raise(unity::shell::application::MirSurfaceInterface *surface);
156+ void moveSurface(int from, int to);
157+ void prependSurfaceHelper(unity::shell::application::MirSurfaceInterface *surface,
158+ unity::shell::application::ApplicationInfoInterface *application);
159+ void connectSurface(unity::shell::application::MirSurfaceInterface *surface);
160+ int generateId();
161+ int nextFreeId(int candidateId);
162+ QString toString();
163+ void onSurfaceDestroyed(unity::shell::application::MirSurfaceInterface *surface);
164+ void onSurfaceDied(unity::shell::application::MirSurfaceInterface *surface);
165+ void removeAt(int index);
166+ void findApplicationRole();
167+
168+ unity::shell::application::ApplicationInfoInterface *getApplicationFromModelAt(int index);
169+
170+ /*
171+ Placeholder for a future surface from a starting or running application.
172+ Enables shell to give immediate feedback to the user by showing, eg,
173+ a splash screen.
174+
175+ It's a model row containing a null surface and the given application.
176+ */
177+ void prependPlaceholder(unity::shell::application::ApplicationInfoInterface *application);
178+
179+ /*
180+ Adds a model row with the given surface and application
181+
182+ Alternatively, if a placeholder exists for the given application it's
183+ filled with the given surface instead.
184+ */
185+ void prependSurface(unity::shell::application::MirSurfaceInterface *surface,
186+ unity::shell::application::ApplicationInfoInterface *application);
187+
188+ struct ModelEntry {
189+ ModelEntry(unity::shell::application::MirSurfaceInterface *surface, unity::shell::application::ApplicationInfoInterface *application, int id)
190+ : surface(surface), application(application), id(id) {}
191+ unity::shell::application::MirSurfaceInterface *surface;
192+ unity::shell::application::ApplicationInfoInterface *application;
193+ int id;
194+ bool removeOnceSurfaceDestroyed{false};
195+ };
196+
197+ QList<ModelEntry> m_surfaceList;
198+ int m_nextId{1};
199+ static const int m_maxId{1000000};
200+
201+ // applications that are being monitored
202+ QList<unity::shell::application::ApplicationInfoInterface *> m_applications;
203+
204+ QAbstractListModel* m_applicationsModel{nullptr};
205+ int m_applicationRole{-1};
206+};
207+
208+Q_DECLARE_METATYPE(TopLevelSurfaceList*)
209+Q_DECLARE_METATYPE(QAbstractListModel*)
210+
211+#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+2−9
@@ -57,16 +57,9 @@ 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
62+ when: target != null && target.live
7063 property: "keymap"
7164 value: root.currentKeymap
7265 }
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+64−51
@@ -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,45 @@ 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+ _makeGreeterReactToAppChange(mainApp.appId);
105+ }
106+
107+ }
108+ Connections {
109+ target: ApplicationManager
110+ onFocusRequested: {
111+ if (shell.mainApp && shell.mainApp.appId === appId) {
112+ _makeGreeterReactToAppChange(appId);
113+ }
114+ }
115+ }
116+ function _makeGreeterReactToAppChange(appId) {
117+ if (tutorial.running && appId != "" && appId != "unity8-dash") {
118+ // If this happens on first boot, we may be in edge
119+ // tutorial or wizard while receiving a call. But a call
120+ // is more important than wizard so just bail out of those.
121+ tutorial.finish();
122+ wizard.hide();
123+ }
124+
125+ if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
126+ // If we are in the middle of a call, make dialer lockedApp and show it.
127+ // This can happen if user backs out of dialer back to greeter, then
128+ // launches dialer again.
129+ greeter.lockedApp = appId;
130+ }
131+ greeter.notifyAppFocusRequested(appId);
132+
133+ panel.indicators.hide();
134+ launcher.hide();
135+ }
136+
137+ // For autopilot consumption
138+ readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
105139
106140 // Disable everything while greeter is waiting, so that the user can't swipe
107141 // the greeter or launcher until we know whether the session is locked.
@@ -198,36 +232,10 @@ Item {
198232 width: parent.width
199233 height: parent.height
200234
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− }
235+ TopLevelSurfaceList {
236+ id: topLevelSurfaceList
237+ objectName: "topLevelSurfaceList"
238+ applicationsModel: ApplicationManager
231239 }
232240
233241 Loader {
@@ -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,8 +569,7 @@ Item {
555569 }
556570 }
557571 onLauncherApplicationSelected: {
558− greeter.notifyAboutToFocusApp(appId);
559− shell.activateApplication(appId);
572+ greeter.notifyUserRequestedApp(appId);
560573 }
561574 onShownChanged: {
562575 if (shown) {
@@ -719,7 +732,7 @@ Item {
719732 onMouseMoved: { cursor.opacity = 1; }
720733 }
721734
722− // non-visual item
735+ // non-visual object
723736 KeymapSwitcher {}
724737
725738 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+106−68
@@ -20,35 +20,73 @@ 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 : 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+ if (surface) {
59+ surfaceContainer.surface = surface;
60+ d.liveSurface = surface.live;
61+ d.hadSurface = false;
62+ surfaceInitTimer.start();
63+ } else {
64+ if (d.surfaceInitialized) {
65+ d.hadSurface = true;
66+ }
67+ d.surfaceInitialized = false;
68+ surfaceContainer.surface = null;
69+ }
70+ }
4871
4972 QtObject {
5073 id: d
5174
75+ property bool liveSurface: false;
76+ property var con: Connections {
77+ target: root.surface
78+ onLiveChanged: d.liveSurface = root.surface.live
79+ }
80+ // using liveSurface instead of root.surface.live because with the latter
81+ // this expression is not reevaluated when root.surface.live changes
82+ property bool needToTakeScreenshot: root.surface && d.surfaceInitialized && !d.liveSurface
83+ && applicationState !== ApplicationInfoInterface.Running
84+ onNeedToTakeScreenshotChanged: {
85+ if (needToTakeScreenshot && screenshotImage.status === Image.Null) {
86+ screenshotImage.take();
87+ }
88+ }
89+
5290 // helpers so that we don't have to check for the existence of an application everywhere
5391 // (in order to avoid breaking qml binding due to a javascript exception)
5492 readonly property string name: root.application ? root.application.name : ""
@@ -62,17 +100,7 @@ FocusScope {
62100 readonly property color splashColorFooter: root.application ? root.application.splashColorFooter : "#00000000"
63101
64102 // 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− }
103+ property bool hadSurface: false
76104
77105 //FIXME - this is a hack to avoid the first few rendered frames as they
78106 // might show the UI accommodating due to surface resizes on startup.
@@ -99,7 +127,9 @@ FocusScope {
99127 Timer {
100128 id: surfaceInitTimer
101129 interval: 100
102− onTriggered: { if (sessionContainer.surface) {d.surfaceInitialized = true;} }
130+ onTriggered: {
131+ if (root.surface && root.surface.live) {d.surfaceInitialized = true;}
132+ }
103133 }
104134
105135 Timer {
@@ -117,7 +147,7 @@ FocusScope {
117147 function take() {
118148 // Save memory by using a half-resolution (thus quarter size) screenshot.
119149 // Do not make this a binding, we can only take the screenshot once!
120− sessionContainer.grabToImage(
150+ surfaceContainer.grabToImage(
121151 function(result) {
122152 screenshotImage.source = result.url;
123153 },
@@ -144,46 +174,33 @@ FocusScope {
144174 }
145175 }
146176
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−
177+ SurfaceContainer {
178+ id: surfaceContainer
152179 requestedWidth: root.requestedWidth
153180 requestedHeight: root.requestedHeight
154−
155181 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−
165182 focus: true
166183 }
167184
168− // SessionContainer size drives ApplicationWindow size
185+ // SurfaceContainer size drives ApplicationWindow size
169186 Binding {
170187 target: root; property: "width"
171− value: stateGroup.state === "surface" ? sessionContainer.width : root.requestedWidth
188+ value: stateGroup.state === "surface" ? surfaceContainer.width : root.requestedWidth
172189 when: root.requestedWidth >= 0
173190 }
174191 Binding {
175192 target: root; property: "height"
176− value: stateGroup.state === "surface" ? sessionContainer.height : root.requestedHeight
193+ value: stateGroup.state === "surface" ? surfaceContainer.height : root.requestedHeight
177194 when: root.requestedHeight >= 0
178195 }
179196
180− // ApplicationWindow size drives SessionContainer size
197+ // ApplicationWindow size drives SurfaceContainer size
181198 Binding {
182− target: sessionContainer; property: "width"; value: root.width
199+ target: surfaceContainer; property: "width"; value: root.width
183200 when: root.requestedWidth < 0
184201 }
185202 Binding {
186− target: sessionContainer; property: "height"; value: root.height
203+ target: surfaceContainer; property: "height"; value: root.height
187204 when: root.requestedHeight < 0
188205 }
189206
@@ -194,32 +211,43 @@ FocusScope {
194211 State {
195212 name: "void"
196213 when:
197− d.hadSurface && (!sessionContainer.surface || !d.surfaceInitialized)
214+ d.hadSurface && (!root.surface || !d.surfaceInitialized)
198215 &&
199216 screenshotImage.status !== Image.Ready
200217 },
201218 State {
202219 name: "splashScreen"
203220 when:
204− !d.hadSurface && (!sessionContainer.surface || !d.surfaceInitialized)
221+ !d.hadSurface && (!root.surface || !d.surfaceInitialized)
205222 &&
206223 screenshotImage.status !== Image.Ready
207224 },
208225 State {
209226 name: "surface"
210227 when:
211− (sessionContainer.surface && d.surfaceInitialized)
228+ (root.surface && d.surfaceInitialized)
212229 &&
213− (d.applicationState !== ApplicationInfoInterface.Stopped
214− || screenshotImage.status !== Image.Ready)
230+ (d.liveSurface ||
231+ (d.applicationState !== ApplicationInfoInterface.Running
232+ && screenshotImage.status !== Image.Ready))
215233 },
216234 State {
217235 name: "screenshot"
218236 when:
219237 screenshotImage.status === Image.Ready
220238 &&
221− (d.applicationState === ApplicationInfoInterface.Stopped
222− || !sessionContainer.surface || !d.surfaceInitialized)
239+ (d.applicationState !== ApplicationInfoInterface.Running
240+ || !root.surface || !d.surfaceInitialized)
241+ },
242+ State {
243+ // This is a dead end. From here we expect the surface to be removed from the model
244+ // shortly after we stop referencing to it in our SurfaceContainer.
245+ name: "closed"
246+ when:
247+ // The surface died while the application is running. It must have been closed
248+ // by the shell or the application decided to destroy it by itself
249+ root.surface && d.surfaceInitialized && !d.liveSurface
250+ && d.applicationState === ApplicationInfoInterface.Running
223251 }
224252 ]
225253
@@ -227,17 +255,17 @@ FocusScope {
227255 Transition {
228256 from: ""; to: "splashScreen"
229257 PropertyAction { target: splashLoader; property: "active"; value: true }
230− PropertyAction { target: sessionContainer.surfaceContainer
258+ PropertyAction { target: surfaceContainer
231259 property: "visible"; value: false }
232260 },
233261 Transition {
234262 from: "splashScreen"; to: "surface"
235263 SequentialAnimation {
236− PropertyAction { target: sessionContainer.surfaceContainer
264+ PropertyAction { target: surfaceContainer
237265 property: "opacity"; value: 0.0 }
238− PropertyAction { target: sessionContainer.surfaceContainer
266+ PropertyAction { target: surfaceContainer
239267 property: "visible"; value: true }
240− UbuntuNumberAnimation { target: sessionContainer.surfaceContainer; property: "opacity";
268+ UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
241269 from: 0.0; to: 1.0
242270 duration: UbuntuAnimation.BriskDuration }
243271 ScriptAction { script: {
@@ -253,12 +281,12 @@ FocusScope {
253281 surfaceIsOldTimer.stop();
254282 d.surfaceOldEnoughToBeResized = false;
255283 splashLoader.active = true;
256− sessionContainer.surfaceContainer.visible = true;
284+ surfaceContainer.visible = true;
257285 } }
258286 UbuntuNumberAnimation { target: splashLoader; property: "opacity";
259287 from: 0.0; to: 1.0
260288 duration: UbuntuAnimation.BriskDuration }
261− PropertyAction { target: sessionContainer.surfaceContainer
289+ PropertyAction { target: surfaceContainer
262290 property: "visible"; value: false }
263291 }
264292 },
@@ -274,15 +302,16 @@ FocusScope {
274302 from: 0.0; to: 1.0
275303 duration: UbuntuAnimation.BriskDuration }
276304 ScriptAction { script: {
277− sessionContainer.surfaceContainer.visible = false;
278− if (sessionContainer.session) { sessionContainer.session.release(); }
305+ surfaceContainer.visible = false;
306+ surfaceContainer.surface = null;
307+ d.hadSurface = true;
279308 } }
280309 }
281310 },
282311 Transition {
283312 from: "screenshot"; to: "surface"
284313 SequentialAnimation {
285− PropertyAction { target: sessionContainer.surfaceContainer
314+ PropertyAction { target: surfaceContainer
286315 property: "visible"; value: true }
287316 UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
288317 from: 1.0; to: 0.0
@@ -310,22 +339,31 @@ FocusScope {
310339 ScriptAction { script: {
311340 surfaceIsOldTimer.stop();
312341 d.surfaceOldEnoughToBeResized = false;
313− sessionContainer.surfaceContainer.visible = false;
314− if (sessionContainer.session) { sessionContainer.session.release(); }
342+ surfaceContainer.visible = false;
315343 } }
316344 },
317345 Transition {
318346 from: "void"; to: "surface"
319347 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";
348+ PropertyAction { target: surfaceContainer; property: "opacity"; value: 0.0 }
349+ PropertyAction { target: surfaceContainer; property: "visible"; value: true }
350+ UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
323351 from: 0.0; to: 1.0
324352 duration: UbuntuAnimation.BriskDuration }
325353 ScriptAction { script: {
326354 surfaceIsOldTimer.start();
327355 } }
328356 }
357+ },
358+ Transition {
359+ to: "closed"
360+ SequentialAnimation {
361+ ScriptAction { script: {
362+ surfaceContainer.visible = false;
363+ surfaceContainer.surface = null;
364+ d.hadSurface = true;
365+ } }
366+ }
329367 }
330368 ]
331369 }
qml/Stages/DecoratedWindow.qml+4−4
@@ -26,8 +26,8 @@ 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
3232 property alias title: decoration.title
3333 property alias fullscreen: applicationWindow.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+20−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,7 @@ FocusScope {
324326 Label {
325327 Layout.fillWidth: true
326328 Layout.preferredHeight: units.gu(6)
327− text: model.name
329+ text: model.surface ? model.surface.name : ""
328330 wrapMode: Text.WordWrap
329331 elide: Text.ElideRight
330332 maximumLineCount: 2
@@ -354,7 +356,7 @@ FocusScope {
354356 anchors.margins: -units.gu(2)
355357 onClicked: {
356358 spreadRepeater.closingIndex = index;
357− ApplicationManager.stopApplication(model.appId)
359+ model.surface.close();
358360 }
359361 }
360362 }
@@ -421,7 +423,7 @@ FocusScope {
421423 objectName: "spreadFlickable"
422424 anchors.fill: parent
423425 property int minContentWidth: 6 * Math.min(height / 4, width / 5)
424− contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
426+ contentWidth: Math.max(6, topLevelSurfaceList.count) * Math.min(height / 4, width / 5)
425427 enabled: false
426428
427429 function snapTo(contentX) {
@@ -523,7 +525,7 @@ FocusScope {
523525 Label {
524526 id: currentSelectedLabel
525527 anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
526− text: spreadRepeater.highlightedIndex >= 0 ? ApplicationManager.get(spreadRepeater.highlightedIndex).name : ""
528+ text: spreadRepeater.highlightedIndex >= 0 ? topLevelSurfaceList.surfaceAt(spreadRepeater.highlightedIndex).name : ""
527529 visible: false
528530 fontSize: "large"
529531 }
@@ -545,7 +547,7 @@ FocusScope {
545547 from: "*"
546548 to: "altTab"
547549 SequentialAnimation {
548− PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
550+ PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(topLevelSurfaceList.count - 1, 1) }
549551 PauseAnimation { duration: spreadContainer.animateIn ? 0 : 140 }
550552 PropertyAction { target: workspaceSelector; property: "visible" }
551553 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+110−82
@@ -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,19 +94,31 @@ 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+ }
115+ updateForegroundMaximizedApp();
131116 }
132− onFocusedAppDelegateChanged: updateForegroundMaximizedApp();
133117
134118 property int foregroundMaximizedAppZ: -1
135119 property int foregroundMaximizedAppIndex: -1 // for stuff like drop shadow and focusing maximized app by clicking panel
136120
121+
137122 function updateForegroundMaximizedApp() {
138123 var tmp = -1;
139124 var tmpAppId = -1;
@@ -148,15 +133,6 @@ AbstractStage {
148133 foregroundMaximizedAppIndex = tmpAppId;
149134 }
150135
151− function indexOf(appId) {
152− for (var i = 0; i < ApplicationManager.count; i++) {
153− if (ApplicationManager.get(i).appId == appId) {
154− return i;
155− }
156− }
157− return -1;
158− }
159−
160136 function minimizeAllWindows() {
161137 for (var i = 0; i < appRepeater.count; i++) {
162138 var appDelegate = appRepeater.itemAt(i);
@@ -165,15 +141,14 @@ AbstractStage {
165141 }
166142 }
167143
168− ApplicationManager.unfocusCurrentApplication(); // no app should have focus at this point
144+ root.applicationManager.unfocusCurrentApplication(); // no app should have focus at this point
169145 }
170146
171147 function focusNext() {
172− ApplicationManager.unfocusCurrentApplication();
173148 for (var i = 0; i < appRepeater.count; i++) {
174149 var appDelegate = appRepeater.itemAt(i);
175150 if (appDelegate && !appDelegate.minimized) {
176− ApplicationManager.focusApplication(appDelegate.appId);
151+ appDelegate.focus = true;
177152 return;
178153 }
179154 }
@@ -182,15 +157,9 @@ AbstractStage {
182157
183158 Connections {
184159 target: PanelState
185− onClose: {
186− ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
187− }
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− }
160+ onClose: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
161+ onMinimize: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.minimize(); } }
162+ onMaximize: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.restoreFromMaximized(); } }
194163 }
195164
196165 Binding {
@@ -227,6 +196,26 @@ 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+ }
230219
231220 FocusScope {
232221 id: appContainer
@@ -244,18 +233,23 @@ AbstractStage {
244233
245234 Repeater {
246235 id: appRepeater
247− model: ApplicationManager
236+ model: topLevelSurfaceList
248237 objectName: "appRepeater"
249238
250239 delegate: FocusScope {
251240 id: appDelegate
252− objectName: "appDelegate_" + appId
241+ objectName: "appDelegate_" + model.id
253242 // z might be overriden in some cases by effects, but we need z ordering
254243 // to calculate occlusion detection
255− property int normalZ: ApplicationManager.count - index
244+ property int normalZ: topLevelSurfaceList.count - index
256245 z: normalZ
246+ onZChanged: {
247+ if (visuallyMaximized) {
248+ priv.updateForegroundMaximizedApp();
249+ }
250+ }
257251 y: PanelState.panelHeight
258− focus: appId === priv.focusedAppId
252+
259253 width: decoratedWindow.width
260254 height: decoratedWindow.height
261255 property int requestedWidth: -1
@@ -280,16 +274,51 @@ AbstractStage {
280274 readonly property alias minimized: appDelegatePrivate.minimized
281275 readonly property alias fullscreen: decoratedWindow.fullscreen
282276
283− readonly property string appId: model.appId
277+ readonly property var application: model.application
284278 property bool animationsEnabled: true
285279 property alias title: decoratedWindow.title
286− readonly property string appName: model.name
280+ readonly property string appName: model.application ? model.application.name : ""
287281 property bool visuallyMaximized: false
288282 property bool visuallyMinimized: false
289283
284+ readonly property var surface: model.surface
285+
286+ Connections {
287+ target: model.surface
288+ onFocusRequested: {
289+ if (spread.state == "altTab") {
290+ spread.cancel();
291+ }
292+ appDelegate.restore();
293+ }
294+ }
295+
290296 onFocusChanged: {
291− if (focus && ApplicationManager.focusedApplicationId !== appId) {
292− ApplicationManager.focusApplication(appId);
297+ if (focus) {
298+ priv.focusedAppDelegate = appDelegate;
299+ topLevelSurfaceList.move(model.id, 0);
300+ }
301+ }
302+ Component.onCompleted: {
303+ // a top level window is always the focused one when it first appears, unfocusing
304+ // any preexisting one
305+ if (index == 0) {
306+ focus = true;
307+ }
308+ }
309+ Component.onDestruction: {
310+ if (focus) {
311+ // focus some other window
312+ for (var i = 0; i < appRepeater.count; i++) {
313+ var appDelegate = appRepeater.itemAt(i);
314+ if (appDelegate && !appDelegate.minimized && i != index) {
315+ appDelegate.focus = true;
316+ return;
317+ }
318+ }
319+ }
320+ if (visuallyMaximized) {
321+ priv.updateForegroundMaximizedApp();
293322 }
294323 }
295324
@@ -301,15 +330,12 @@ AbstractStage {
301330 decoratedWindow.fullscreen ||
302331 (spread.state == "altTab" && index === spread.highlightedIndex)
303332
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
333+ function close() {
334+ model.surface.close();
335+ // TODO: Should we brute force and stop the application if it doesn't quit
336+ // after having its last window closed?
337+ // TODO: Destroy the surface if the application doesn't comply after a given
338+ // amount of time.
313339 }
314340
315341 function maximize(animated) {
@@ -351,7 +377,8 @@ AbstractStage {
351377 maximizeLeft();
352378 else if (maximizedRight)
353379 maximizeRight();
354− ApplicationManager.focusApplication(appId);
380+
381+ focus = true;
355382 }
356383
357384 function playFocusAnimation() {
@@ -486,7 +513,7 @@ AbstractStage {
486513 id: previewBinding
487514 target: appDelegate
488515 property: "z"
489− value: ApplicationManager.count + 1
516+ value: topLevelSurfaceList.count + 1
490517 when: index == spread.highlightedIndex && spread.ready
491518 }
492519
@@ -497,12 +524,12 @@ AbstractStage {
497524 minWidth: units.gu(10)
498525 minHeight: units.gu(10)
499526 borderThickness: units.gu(2)
500− windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
527+ windowId: model.application.appId // FIXME: Change this to point to windowId once we have such a thing
501528 screenWidth: appContainer.width
502529 screenHeight: appContainer.height
503530 leftMargin: root.leftMargin
504531
505− onPressed: { ApplicationManager.focusApplication(model.appId) }
532+ onPressed: { appDelegate.focus = true; }
506533
507534 Component.onCompleted: {
508535 loadWindowState();
@@ -529,24 +556,25 @@ AbstractStage {
529556 objectName: "decoratedWindow"
530557 anchors.left: appDelegate.left
531558 anchors.top: appDelegate.top
532− application: ApplicationManager.get(index)
533− active: ApplicationManager.focusedApplicationId === model.appId
559+ application: model.application
560+ surface: model.surface
561+ active: appDelegate.focus
534562 focus: true
535563
536564 requestedWidth: appDelegate.requestedWidth
537565 requestedHeight: appDelegate.requestedHeight
538566
539− onClose: ApplicationManager.stopApplication(model.appId)
567+ onClose: { appDelegate.close(); }
540568 onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
541569 ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
542570 onMinimize: appDelegate.minimize()
543− onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
571+ onDecorationPressed: { appDelegate.focus = true; }
544572 }
545573
546574 WindowedFullscreenPolicy {
547575 id: fullscreenPolicy
548576 active: true
549− application: decoratedWindow.application
577+ surface: model.surface
550578 }
551579 }
552580 }
qml/Stages/PhoneStage.qml+124−84
@@ -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,25 @@ AbstractStage {
258210 }
259211 }
260212
213+ Instantiator {
214+ model: root.applicationManager
215+ delegate: Binding {
216+ readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
217+ target: model.application
218+ property: "requestedState"
219+ value: (isDash && root.keepDashRunning)
220+ || (!root.suspended && model.application && priv.focusedAppId === model.application.appId)
221+ ? ApplicationInfoInterface.RequestedRunning
222+ : ApplicationInfoInterface.RequestedSuspended
223+ }
224+ }
225+
226+ Binding {
227+ target: MirFocusController
228+ property: "focusedSurface"
229+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
230+ }
231+
261232 Flickable {
262233 id: spreadView
263234 objectName: "spreadView"
@@ -308,13 +279,37 @@ AbstractStage {
308279 // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
309280 // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
310281 // the Flickable.boundsBehavior upon release).
311− onContentXChanged: { forceItToRemainStillIfBeingResized(); }
282+ onContentXChanged: {
283+ if (!undoContentXReset()) {
284+ forceItToRemainStillIfBeingResized();
285+ }
286+ }
312287 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
313288 function forceItToRemainStillIfBeingResized() {
314289 if (root.beingResized && contentX != -spreadView.shift) {
315290 contentX = -spreadView.shift;
316291 }
317292 }
293+ function undoContentXReset() {
294+ if (contentWidth === -1) {
295+ contentWidthOnLastContentXChange = contentWidth;
296+ lastContentX = contentX;
297+ return false;
298+ }
299+
300+ if (contentWidth != contentWidthOnLastContentXChange
301+ && lastContentX == -shift && contentX == 0) {
302+ // Flickable is resetting contentX because contentWidth has changed. Undo it.
303+ contentX = -shift;
304+ return true;
305+ }
306+
307+ contentWidthOnLastContentXChange = contentWidth;
308+ lastContentX = contentX;
309+ return false;
310+ }
311+ property real contentWidthOnLastContentXChange: -1
312+ property real lastContentX: 0
318313
319314 Behavior on contentX {
320315 enabled: root.altTabPressed
@@ -377,7 +372,7 @@ AbstractStage {
377372 snapAnimation.start();
378373 return;
379374 }
380− if (root.applicationManager.count <= index) {
375+ if (topLevelSurfaceList.count <= index) {
381376 // In case we're trying to snap to some non existing app, lets snap back to the first one
382377 index = 0;
383378 }
@@ -414,7 +409,8 @@ AbstractStage {
414409 ScriptAction {
415410 script: {
416411 if (spreadView.selectedIndex >= 0) {
417− root.applicationManager.focusApplication(root.applicationManager.get(spreadView.selectedIndex).appId);
412+ var delegate = spreadRepeater.itemAt(spreadView.selectedIndex)
413+ delegate.focus = true;
418414
419415 spreadView.selectedIndex = -1;
420416 spreadView.phase = 0;
@@ -429,7 +425,7 @@ AbstractStage {
429425 // This width controls how much the spread can be flicked left/right. It's composed of:
430426 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
431427 // + 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
428+ width: Math.max(3, topLevelSurfaceList.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
433429 height: parent.height
434430 Behavior on width {
435431 enabled: spreadView.closingIndex >= 0
@@ -452,10 +448,33 @@ AbstractStage {
452448 Repeater {
453449 id: spreadRepeater
454450 objectName: "spreadRepeater"
455− model: root.applicationManager
451+ model: topLevelSurfaceList
452+
453+ onItemAdded: {
454+ if (spreadView.phase == 2) {
455+ spreadView.snapTo(spreadRepeater.count - 1);
456+ }
457+ }
458+
459+ onItemRemoved: {
460+ // Unless we're closing the app ourselves in the spread,
461+ // lets make sure the spread doesn't mess up by the changing app list.
462+ if (spreadView.closingIndex == -1) {
463+ spreadView.phase = 0;
464+ spreadView.contentX = -spreadView.shift;
465+ focusTopMostApp();
466+ }
467+ }
468+ function focusTopMostApp() {
469+ if (spreadRepeater.count > 0) {
470+ var topmostDelegate = spreadRepeater.itemAt(0);
471+ topmostDelegate.focus = true;
472+ }
473+ }
474+
456475 delegate: TransformedSpreadDelegate {
457476 id: appDelegate
458− objectName: "appDelegate" + index
477+ objectName: "spreadDelegate_" + model.id
459478 startAngle: 45
460479 endAngle: 5
461480 startScale: root.startScale
@@ -467,28 +486,44 @@ AbstractStage {
467486 selected: spreadView.selectedIndex == index
468487 otherSelected: spreadView.selectedIndex >= 0 && !selected
469488 interactive: !spreadView.interactive && spreadView.phase === 0
470− && priv.fullyShowingFocusedApp && root.interactive && isFocused
489+ && priv.fullyShowingFocusedApp && root.interactive && focus
471490 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
472491 maximizedAppTopMargin: root.maximizedAppTopMargin
473492 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
474493 focusFirstApp: root.focusFirstApp
475494 highlightShown: root.altTabPressed && index === priv.highlightIndex
476495
477− readonly property bool isDash: model.appId == "unity8-dash"
496+ readonly property bool isDash: model.application.appId == "unity8-dash"
478497
479− Binding {
480− target: appDelegate.application
481− property: "exemptFromLifecycle"
482− value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
498+ Component.onCompleted: {
499+ // a top level window is always the focused one when it first appears, unfocusing
500+ // any preexisting one
501+ if (model.index == 0) {
502+ focus = true;
503+ }
504+ }
505+
506+ onFocusChanged: {
507+ if (focus) {
508+ priv.focusedAppDelegate = appDelegate;
509+ topLevelSurfaceList.move(model.id, 0);
510+ }
511+ }
512+ Connections {
513+ target: model.surface
514+ onFocusRequested: {
515+ if (spreadView.phase > 0) {
516+ spreadView.snapTo(model.index);
517+ } else {
518+ appDelegate.focus = true;
519+ }
520+ }
483521 }
484522
485523 Binding {
486524 target: appDelegate.application
487− property: "requestedState"
488− value: (isDash && root.keepDashRunning)
489− || (!root.suspended && appDelegate.focus)
490− ? ApplicationInfoInterface.RequestedRunning
491− : ApplicationInfoInterface.RequestedSuspended
525+ property: "exemptFromLifecycle"
526+ value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
492527 }
493528
494529 z: isDash && !spreadView.active ? -1 : behavioredIndex
@@ -509,7 +544,8 @@ AbstractStage {
509544 return spreadView.width + spreadIndex * spreadView.tileDistance;
510545 }
511546
512− application: root.applicationManager.get(index)
547+ application: model.application
548+ surface: model.surface
513549 closeable: !isDash
514550
515551 property real behavioredIndex: index
@@ -593,11 +629,7 @@ AbstractStage {
593629
594630 onClicked: {
595631 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− }
632+ spreadView.snapTo(index);
601633 }
602634 }
603635
@@ -611,7 +643,15 @@ AbstractStage {
611643
612644 onClosed: {
613645 spreadView.closingIndex = index;
614− root.applicationManager.stopApplication(root.applicationManager.get(index).appId);
646+ if (appDelegate.surface) {
647+ appDelegate.surface.close();
648+ } else if (appDelegate.application) {
649+ root.applicationManager.stopApplication(appDelegate.application.appId);
650+ } else {
651+ // should never happen
652+ console.warn("Can't close topLevelSurfaceList entry as it has neither"
653+ + " a surface nor an application");
654+ }
615655 }
616656
617657 Binding {
@@ -637,7 +677,7 @@ AbstractStage {
637677 onStageAboutToBeUnloaded: fullscreenPolicy.active = false
638678 }
639679 }
640− }
680+ } // Repeater {
641681 }
642682 }
643683
qml/Stages/PromptSurfaceAnimations.qmlnew file+77
@@ -0,0 +1,77 @@
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+ SequentialAnimation {
43+ // clip so we don't go out of parent's bounds during spread
44+ PropertyAction { target: root.container.parent; property: "clip"; value: true }
45+ UbuntuNumberAnimation { target: root.surfaceItem; property: "y"; to: root.container.height
46+ duration: UbuntuAnimation.BriskDuration }
47+ PropertyAction { target: root.surfaceItem; property: "visible"; value: false }
48+ PropertyAction { target: container.parent; property: "clip"; value: false }
49+ ScriptAction { script: {
50+ root.surfaceItem.surface = null;
51+ } }
52+ }
53+ },
54+ Transition {
55+ from: "*"; to: "ready"
56+ SequentialAnimation {
57+ // clip so we don't go out of parent's bounds during spread
58+ PropertyAction { target: root.container.parent; property: "clip"; value: true }
59+ ScriptAction { script: {
60+ root.surfaceItem.y = root.container.height;
61+ root.surfaceItem.visible = true;
62+ } }
63+ UbuntuNumberAnimation {
64+ target: root.surfaceItem; property: "y"; to: 0
65+ duration: UbuntuAnimation.BriskDuration
66+ }
67+ PropertyAction { target: container.parent; property: "clip"; value: false }
68+ }
69+ },
70+ Transition {
71+ from: "*"; to: "blank"
72+ ScriptAction { script: {
73+ root.surfaceItem.visible = false;
74+ } }
75+ }
76+ ]
77+}
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/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 in some cases
40+ // we remove the surface from the surfaceItem programatically,
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+281−223
@@ -32,14 +32,15 @@ AbstractStage {
3232
3333 // Functions to be called from outside
3434 function updateFocusedAppOrientation() {
35− var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
36− if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
37− spreadRepeater.itemAt(mainStageAppIndex).matchShellOrientation();
35+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
36+
37+ if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
38+ spreadRepeater.itemAt(mainStageIndex).matchShellOrientation();
3839 }
3940
4041 for (var i = 0; i < spreadRepeater.count; ++i) {
4142
42− if (i === mainStageAppIndex) {
43+ if (i === mainStageIndex) {
4344 continue;
4445 }
4546
@@ -60,16 +61,14 @@ AbstractStage {
6061 }
6162 }
6263 function updateFocusedAppOrientationAnimated() {
63− var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
64− if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
65− spreadRepeater.itemAt(mainStageAppIndex).animateToShellOrientation();
64+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
65+ if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
66+ spreadRepeater.itemAt(mainStageIndex).animateToShellOrientation();
6667 }
6768
68− if (priv.sideStageAppId) {
69− var sideStageAppIndex = priv.indexOf(priv.sideStageAppId);
70− if (sideStageAppIndex >= 0 && sideStageAppIndex < spreadRepeater.count) {
71− spreadRepeater.itemAt(sideStageAppIndex).matchShellOrientation();
72− }
69+ var sideStageIndex = root.topLevelSurfaceList.indexForId(priv.sideStageItemId);
70+ if (sideStageIndex >= 0 && sideStageIndex < spreadRepeater.count) {
71+ spreadRepeater.itemAt(sideStageIndex).matchShellOrientation();
7372 }
7473 }
7574
@@ -81,11 +80,20 @@ AbstractStage {
8180
8281 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
8382
83+ mainApp: {
84+ if (priv.mainStageItemId > 0) {
85+ var index = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
86+ return root.topLevelSurfaceList.applicationAt(index);
87+ } else {
88+ return null;
89+ }
90+ }
91+
8492 supportedOrientations: {
8593 if (mainApp) {
8694 var orientations = mainApp.supportedOrientations;
8795 orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
88− if (priv.sideStageAppId && !spreadView.surfaceDragging) {
96+ if (priv.sideStageItemId && !spreadView.surfaceDragging) {
8997 // If we have a sidestage app, support Portrait orientation
9098 // so that it will switch the sidestage app to mainstage on rotate
9199 orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
@@ -116,7 +124,7 @@ AbstractStage {
116124 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
117125 // left edge drag released. Minimum distance is given by design.
118126 if (priv.oldInverseProgress > units.gu(22)) {
119− ApplicationManager.requestFocusApplication("unity8-dash");
127+ root.applicationManager.requestFocusApplication("unity8-dash");
120128 }
121129 }
122130 priv.oldInverseProgress = inverseProgress;
@@ -154,17 +162,56 @@ AbstractStage {
154162 }
155163 }
156164
165+ Connections {
166+ target: root.topLevelSurfaceList
167+ onListChanged: priv.updateMainAndSideStageIndexes()
168+ }
169+
170+ onTopLevelSurfaceListChanged: {
171+ // Initialize their values
172+ priv.updateMainAndSideStageIndexes();
173+ }
174+
157175 QtObject {
158176 id: priv
159177 objectName: "stagesPriv"
160178
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
179+ function updateMainAndSideStageIndexes() {
180+ var choseMainStage = false;
181+ var choseSideStage = false;
182+
183+ if (!root.topLevelSurfaceList)
184+ return;
185+
186+ for (var i = 0; i < root.topLevelSurfaceList.count && (!choseMainStage || !choseSideStage); ++i) {
187+ var spreadDelegate = spreadRepeater.itemAt(i);
188+ if (!spreadView.sideStageHidden && spreadDelegate.stage == ApplicationInfoInterface.SideStage
189+ && !choseSideStage) {
190+ priv.sideStageDelegate = spreadDelegate
191+ priv.sideStageItemId = root.topLevelSurfaceList.idAt(i);
192+ priv.sideStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
193+ choseSideStage = true;
194+ } else if (!choseMainStage && spreadDelegate.stage == ApplicationInfoInterface.MainStage) {
195+ priv.mainStageDelegate = spreadDelegate;
196+ priv.mainStageItemId = root.topLevelSurfaceList.idAt(i);
197+ priv.mainStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
198+ choseMainStage = true;
199+ }
200+ }
201+ if (!choseMainStage) {
202+ priv.mainStageDelegate = null;
203+ priv.mainStageItemId = 0;
204+ priv.mainStageAppId = "";
205+ }
206+ if (!choseSideStage) {
207+ priv.sideStageDelegate = null;
208+ priv.sideStageItemId = 0;
209+ priv.sideStageAppId = "";
210+ }
165211 }
166212
167− property string oldFocusedAppId: ""
213+ property var focusedAppDelegate: null
214+
168215 property bool mainAppOrientationChangesEnabled: false
169216
170217 property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
@@ -173,36 +220,21 @@ AbstractStage {
173220 property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
174221 || root.shellOrientation === Qt.InvertedLandscapeOrientation
175222
176− property string mainStageAppId
177− property string sideStageAppId
223+ property var mainStageDelegate: null
224+ property var sideStageDelegate: null
225+
226+ property int mainStageItemId: 0
227+ property int sideStageItemId: 0
178228
179− // For convenience, keep properties of the first two apps in the model
180− property string appId0
181− property string appId1
229+ property string mainStageAppId: ""
230+ property string sideStageAppId: ""
182231
183232 property int oldInverseProgress: 0
184233
185234 property int highlightIndex: 0
186235
187− onFocusedAppIdChanged: updateStageApps()
188−
189− onFocusedAppDelegateChanged: {
190− if (focusedAppDelegate) {
191− focusedAppDelegate.focus = true;
192− }
193− }
194−
195236 property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
196237 (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−
206238 function evaluateOneWayFlick(gesturePoints) {
207239 // Need to have at least 3 points to recognize it as a flick
208240 if (gesturePoints.length < 3) {
@@ -231,88 +263,41 @@ AbstractStage {
231263 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
232264 }
233265
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);
266+ function setAppStage(appId, stage) {
267+ var app = root.applicationManager.findApplication(appId);
246268 if (app) {
247269 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 = "";
264270 }
265−
266− appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
267− appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
268271 }
269272
270273 readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
271274 root.shellOrientation == Qt.InvertedLandscapeOrientation
272− Component.onCompleted: updateStageApps();
273275 }
274276
275− Connections {
276− target: ApplicationManager
277− onFocusRequested: {
278− if (spreadView.interactive) {
279− spreadView.snapTo(priv.indexOf(appId));
280− } else {
281− ApplicationManager.focusApplication(appId);
282− }
277+ Instantiator {
278+ model: root.applicationManager
279+ delegate: Binding {
280+ readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
281+ target: model.application
282+ property: "requestedState"
283+
284+ // NB: the first application clause is just to ensure we never get warnings for trying to access
285+ // members of a null variable.
286+ value: model.application &&
287+ (
288+ (isDash && root.keepDashRunning)
289+ || (!root.suspended && (model.application.appId === priv.mainStageAppId
290+ || model.application.appId === priv.sideStageAppId))
291+ )
292+ ? ApplicationInfoInterface.RequestedRunning
293+ : ApplicationInfoInterface.RequestedSuspended
283294 }
295+ }
284296
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);
292− }
293− }
294−
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− }
297+ Binding {
298+ target: MirFocusController
299+ property: "focusedSurface"
300+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
316301 }
317302
318303 Flickable {
@@ -363,16 +348,45 @@ AbstractStage {
363348 property int selectedIndex: -1
364349 property int draggedDelegateCount: 0
365350 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(); }
351+ property var selectedDelegate: selectedIndex !== -1 ? spreadRepeater.itemAt(selectedIndex) : null
352+
353+ // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
354+ // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
355+ // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
356+ // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
357+ // the Flickable.boundsBehavior upon release).
358+ onContentXChanged: {
359+ if (!undoContentXReset()) {
360+ forceItToRemainStillIfBeingResized();
361+ }
362+ }
370363 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
371364 function forceItToRemainStillIfBeingResized() {
372− if (root.beingResized && contentX != -shift) {
365+ if (root.beingResized && contentX != -spreadView.shift) {
366+ contentX = -spreadView.shift;
367+ }
368+ }
369+ function undoContentXReset() {
370+ if (contentWidth === -1) {
371+ contentWidthOnLastContentXChange = contentWidth;
372+ lastContentX = contentX;
373+ return false;
374+ }
375+
376+ if (contentWidth != contentWidthOnLastContentXChange
377+ && lastContentX == -shift && contentX == 0) {
378+ // Flickable is resetting contentX because contentWidth has changed. Undo it.
373379 contentX = -shift;
380+ return true;
374381 }
382+
383+ contentWidthOnLastContentXChange = contentWidth;
384+ lastContentX = contentX;
385+ return false;
375386 }
387+ property real contentWidthOnLastContentXChange: -1
388+ property real lastContentX: 0
389+ // </FIXME-contentX>
376390
377391 property bool animateX: true
378392 property bool beingResized: root.beingResized
@@ -385,31 +399,33 @@ AbstractStage {
385399 }
386400 }
387401
388− property real sideStageDragProgress: sideStage.progress
389− property bool sideStageVisible: priv.sideStageAppId
390402 property real sideStageWidth: units.gu(40)
391403
392404 property bool surfaceDragging: triGestureArea.recognisedDrag
393405
394− // In case the ApplicationManager already holds an app when starting up we're missing animations
406+ readonly property bool sideStageVisible: priv.sideStageItemId != 0
407+
408+ // In case applicationManager already holds an app when starting up we're missing animations
395409 // Make sure we end up in the same state
396410 Component.onCompleted: {
397411 spreadView.contentX = -spreadView.shift
398412 }
399413
400414 property int nextInStack: {
415+ var mainStageIndex = priv.mainStageDelegate ? priv.mainStageDelegate.index : -1;
416+ var sideStageIndex = priv.sideStageDelegate ? priv.sideStageDelegate.index : -1;
401417 switch (state) {
402418 case "main":
403− if (ApplicationManager.count > 1) {
419+ if (root.topLevelSurfaceList.count > 1) {
404420 return 1;
405421 }
406422 return -1;
407423 case "mainAndOverlay":
408− if (ApplicationManager.count <= 2) {
424+ if (root.topLevelSurfaceList.count <= 2) {
409425 return -1;
410426 }
411− if (priv.appId0 == priv.mainStageAppId || priv.appId0 == priv.sideStageAppId) {
412− if (priv.appId1 == priv.mainStageAppId || priv.appId1 == priv.sideStageAppId) {
427+ if (mainStageIndex == 0 || sideStageIndex == 0) {
428+ if (mainStageIndex == 1 || sideStageIndex == 1) {
413429 return 2;
414430 }
415431 return 1;
@@ -420,7 +436,7 @@ AbstractStage {
420436 }
421437 return -1;
422438 }
423− property int nextZInStack: indexToZIndex(nextInStack)
439+ property int nextZInStack
424440
425441 states: [
426442 State {
@@ -440,13 +456,13 @@ AbstractStage {
440456 }
441457 ]
442458 state: {
443− if ((priv.mainStageAppId && !priv.sideStageAppId) || !priv.sideStageEnabled) {
459+ if ((priv.mainStageItemId && !priv.sideStageItemId) || !priv.sideStageEnabled) {
444460 return "main";
445461 }
446− if (!priv.mainStageAppId && priv.sideStageAppId) {
462+ if (!priv.mainStageItemId && priv.sideStageItemId) {
447463 return "overlay";
448464 }
449− if (priv.mainStageAppId && priv.sideStageAppId) {
465+ if (priv.mainStageItemId && priv.sideStageItemId) {
450466 return "mainAndOverlay";
451467 }
452468 return "empty";
@@ -511,7 +527,7 @@ AbstractStage {
511527 // only shuffle when we've got a main and overlay
512528 if (state !== "mainAndOverlay") return index;
513529
514− var app = ApplicationManager.get(index);
530+ var app = root.topLevelSurfaceList.applicationAt(index);
515531 if (!app) {
516532 return index;
517533 }
@@ -519,20 +535,23 @@ AbstractStage {
519535 // don't shuffle indexes greater than "actives or next"
520536 if (index > 2) return index;
521537
522− if (app.appId === priv.mainStageAppId) {
538+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
539+ var sideStageIndex = root.topLevelSurfaceList.indexForId(priv.sideStageItemId);
540+
541+ if (index == mainStageIndex) {
523542 // Active main stage always at 0
524543 return 0;
525544 }
526545
527546 if (spreadView.nextInStack > 0) {
528− var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
547+ var nextAppInStack = root.topLevelSurfaceList.applicationAt(spreadView.nextInStack);
529548
530549 if (index === spreadView.nextInStack) {
531550 // this is the next app in stack.
532551
533552 if (app.stage === ApplicationInfoInterface.SideStage) {
534553 // 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);
554+ return Math.min(2, root.topLevelSurfaceList.count-1);
536555 }
537556 return 1;
538557 }
@@ -540,9 +559,9 @@ AbstractStage {
540559 // if the next app in stack is a sidestage app, it must order on top of other side stage app
541560 return 1;
542561 }
543− return Math.min(2, ApplicationManager.count-1);
562+ return Math.min(2, root.topLevelSurfaceList.count-1);
544563 }
545− return Math.min(index+1, ApplicationManager.count-1);
564+ return Math.min(index+1, root.topLevelSurfaceList.count-1);
546565 }
547566
548567 SequentialAnimation {
@@ -560,12 +579,13 @@ AbstractStage {
560579 script: {
561580 if (spreadView.selectedIndex >= 0) {
562581 var newIndex = spreadView.selectedIndex;
563− var application = ApplicationManager.get(newIndex);
564− if (application.stage === ApplicationInfoInterface.SideStage) {
582+ var application = root.topLevelSurfaceList.applicationAt(newIndex);
583+ var spreadDelegate = spreadRepeater.itemAt(newIndex);
584+ if (spreadDelegate.stage === ApplicationInfoInterface.SideStage) {
565585 sideStage.showNow();
566586 }
567587 spreadView.selectedIndex = -1;
568− ApplicationManager.focusApplication(application.appId);
588+ spreadDelegate.focus = true;
569589 spreadView.phase = 0;
570590 spreadView.contentX = -spreadView.shift;
571591 }
@@ -581,7 +601,7 @@ AbstractStage {
581601 MouseArea {
582602 id: spreadRow
583603 x: spreadView.contentX
584− width: spreadView.width + Math.max(spreadView.width, ApplicationManager.count * spreadView.tileDistance)
604+ width: spreadView.width + Math.max(spreadView.width, root.topLevelSurfaceList.count * spreadView.tileDistance)
585605 height: root.height
586606
587607 onClicked: {
@@ -599,8 +619,8 @@ AbstractStage {
599619 enabled: priv.sideStageEnabled
600620
601621 onDropped: {
602− priv.setAppStage(drag.source.appId, ApplicationInfoInterface.MainStage, true);
603− ApplicationManager.focusApplication(drag.source.appId);
622+ drop.source.spreadDelegate.stage = ApplicationInfoInterface.MainStage;
623+ drop.source.spreadDelegate.focus = true;
604624 }
605625 keys: "SideStage"
606626 }
@@ -611,10 +631,10 @@ AbstractStage {
611631 height: priv.landscapeHeight
612632 x: spreadView.width - width
613633 z: {
614− if (!priv.mainStageAppId) return 0;
634+ if (!priv.mainStageItemId) return 0;
615635
616− if (priv.sideStageAppId && spreadView.nextInStack > 0) {
617− var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
636+ if (priv.sideStageItemId && spreadView.nextInStack > 0) {
637+ var nextAppInStack = root.topLevelSurfaceList.applicationAt(spreadView.nextInStack);
618638
619639 if (nextAppInStack.stage === ApplicationInfoInterface.MainStage) {
620640 // if the next app in stack is a main stage app, put the sidestage on top of it.
@@ -631,12 +651,11 @@ AbstractStage {
631651 Behavior on opacity { UbuntuNumberAnimation {} }
632652
633653 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);
654+ if (!shown && priv.sideStageDelegate && priv.focusedAppDelegate === priv.sideStageDelegate
655+ && priv.mainStageDelegate) {
656+ priv.mainStageDelegate.focus = true;
657+ } else if (shown && priv.sideStageDelegate) {
658+ priv.sideStageDelegate.focus = true;
640659 }
641660 }
642661
@@ -655,8 +674,8 @@ AbstractStage {
655674 }
656675 onDropped: {
657676 if (drop.keys == "MainStage") {
658− priv.setAppStage(drop.source.appId, ApplicationInfoInterface.SideStage, true);
659− ApplicationManager.requestFocusApplication(drop.source.appId);
677+ drop.source.spreadDelegate.stage = ApplicationInfoInterface.SideStage;
678+ drop.source.spreadDelegate.focus = true;
660679 }
661680 }
662681 drag {
@@ -672,38 +691,85 @@ AbstractStage {
672691 Repeater {
673692 id: spreadRepeater
674693 objectName: "spreadRepeater"
675− model: ApplicationManager
694+ model: root.topLevelSurfaceList
695+
696+ onItemAdded: {
697+ if (spreadView.phase == 2) {
698+ spreadView.snapTo(index);
699+ }
700+ }
701+
702+ onItemRemoved: {
703+ // Unless we're closing the app ourselves in the spread,
704+ // lets make sure the spread doesn't mess up by the changing app list.
705+ if (spreadView.closingIndex == -1) {
706+ spreadView.phase = 0;
707+ spreadView.contentX = -spreadView.shift;
708+ focusTopMostApp();
709+ }
710+ }
711+ function focusTopMostApp() {
712+ if (spreadRepeater.count > 0) {
713+ var topmostDelegate = spreadRepeater.itemAt(0);
714+ topmostDelegate.focus = true;
715+ }
716+ }
676717
677718 delegate: TransformedTabletSpreadDelegate {
678719 id: spreadTile
679− objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
680− : "tabletSpreadDelegate_null";
720+ objectName: "spreadDelegate_" + model.id
721+
722+ property int index: model.index
681723 width: spreadView.width
682724 height: spreadView.height
683− active: appId == priv.mainStageAppId || appId == priv.sideStageAppId
725+ active: model.id == priv.mainStageItemId || model.id == priv.sideStageItemId
684726 zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
727+ onZIndexChanged: {
728+ if (spreadView.nextInStack == model.index) {
729+ spreadView.nextZInStack = zIndex;
730+ }
731+ }
685732 selected: spreadView.selectedIndex == index
686733 otherSelected: spreadView.selectedIndex >= 0 && !selected
687− isInSideStage: priv.sideStageAppId === appId
734+ isInSideStage: priv.sideStageItemId == model.id
688735 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
689736 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
690737 maximizedAppTopMargin: root.maximizedAppTopMargin
691− dragOffset: !isDash && appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
692− application: ApplicationManager.get(index)
738+ dragOffset: !isDash && model.id == priv.mainStageItemId && root.inverseProgress > 0
739+ && spreadView.phase === 0 ? root.inverseProgress : 0
740+ application: model.application
741+ surface: model.surface
693742 closeable: !isDash
694743 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
695744
696− readonly property bool wantsMainStage: model.stage == ApplicationInfoInterface.MainStage
745+ readonly property bool wantsMainStage: stage == ApplicationInfoInterface.MainStage
697746
698− readonly property string appId: model.appId
699− readonly property bool isDash: model.appId == "unity8-dash"
747+ readonly property bool isDash: model.application.appId == "unity8-dash"
748+
749+ onFocusChanged: {
750+ if (focus) {
751+ priv.focusedAppDelegate = spreadTile;
752+ root.topLevelSurfaceList.move(model.id, 0);
753+ }
754+ if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
755+ sideStage.show();
756+ }
757+ }
758+ Connections {
759+ target: model.surface
760+ onFocusRequested: spreadTile.focus = true;
761+ }
700762
701− stage: model.stage
702763 fullscreen: {
703− if (mainApp && stage === ApplicationInfoInterface.SideStage) {
704− return mainApp.fullscreen;
764+ if (priv.mainStageDelegate && stage === ApplicationInfoInterface.SideStage) {
765+ return priv.mainStageDelegate.fullscreen;
766+ } else if (surface) {
767+ return surface.state === Mir.FullscreenState;
768+ } else if (application) {
769+ return application.fullscreen;
770+ } else {
771+ return false;
705772 }
706− return application ? application.fullscreen : false;
707773 }
708774
709775 supportedOrientations: {
@@ -724,21 +790,10 @@ AbstractStage {
724790 }
725791 }
726792
727−
728793 Binding {
729794 target: spreadTile.application
730795 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
796+ value: !model.application.isTouchApp || isExemptFromLifecycle(model.application.appId)
742797 }
743798
744799 // FIXME: A regular binding doesn't update any more after closing an app.
@@ -760,34 +815,35 @@ AbstractStage {
760815 onSideStageEnabledChanged: refreshStage()
761816 }
762817
818+ property bool _constructing: true;
819+ onStageChanged: {
820+ if (!_constructing) {
821+ priv.updateMainAndSideStageIndexes();
822+ }
823+ }
824+
763825 Component.onCompleted: {
764− refreshStage()
765− stageChanged.connect(priv.updateStageApps);
826+ // a top level window is always the focused one when it first appears, unfocusing
827+ // any preexisting one
828+ if (model.index == 0) {
829+ focus = true;
830+ }
831+ refreshStage();
832+ _constructing = false;
833+ }
834+ Component.onDestruction: {
835+ WindowStateStorage.saveStage(model.application.appId, stage);
766836 }
767837
768838 function refreshStage() {
769− var stage = ApplicationInfoInterface.MainStage;
839+ var newStage = ApplicationInfoInterface.MainStage;
770840 if (priv.sideStageEnabled) {
771841 if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
772− stage = WindowStateStorage.getStage(appId);
842+ newStage = WindowStateStorage.getStage(application.appId);
773843 }
774844 }
775845
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− })
846+ stage = newStage;
791847 }
792848
793849 progress: {
@@ -873,12 +929,13 @@ AbstractStage {
873929 script: {
874930 // rotate immediately.
875931 spreadTile.matchShellOrientation();
876− if (ApplicationManager.focusedApplicationId === spreadTile.appId &&
932+ if (priv.focusedAppDelegate === spreadTile &&
877933 priv.sideStageEnabled && !sideStage.shown) {
878934 // Sidestage was focused, so show the side stage.
879935 sideStage.show();
880936 // 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) {
937+ } else if (priv.sideStageDelegate === spreadTile &&
938+ mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
882939 sideStage.hideNow();
883940 }
884941 }
@@ -890,10 +947,10 @@ AbstractStage {
890947 SequentialAnimation {
891948 ScriptAction {
892949 script: {
893− if (priv.sideStageAppId === spreadTile.appId &&
950+ if (priv.sideStageDelegate === spreadTile &&
894951 mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
895952 // The mainstage app did not natively support portrait orientation, so focus the sidestage.
896− ApplicationManager.requestFocusApplication(spreadTile.appId);
953+ spreadTile.focus = true;
897954 }
898955 }
899956 }
@@ -922,28 +979,26 @@ AbstractStage {
922979
923980 onClosed: {
924981 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();
982+ if (spreadTile.surface) {
983+ spreadTile.surface.close();
984+ } else if (spreadTile.application) {
985+ root.applicationManager.stopApplication(spreadTile.application.appId);
986+ } else {
987+ // should never happen
988+ console.warn("Can't close topLevelSurfaceList entry as it has neither"
989+ + " a surface nor an application");
935990 }
936991 }
937992
938993 Binding {
939994 target: root
940− when: model.appId == priv.mainStageAppId
995+ when: model.id == priv.mainStageItemId
941996 property: "mainAppWindowOrientationAngle"
942997 value: appWindowOrientationAngle
943998 }
944999 Binding {
9451000 target: priv
946− when: model.appId == priv.mainStageAppId
1001+ when: model.id == priv.mainStageItemId
9471002 property: "mainAppOrientationChangesEnabled"
9481003 value: orientationChangesEnabled
9491004 }
@@ -973,17 +1028,19 @@ AbstractStage {
9731028 anchors.fill: parent
9741029 enabled: priv.sideStageEnabled && !spreadView.active
9751030 property var dragObject: null
976− property string appId: ""
1031+
1032+ property Item spreadDelegate
1033+
9771034 dragComponent: dragComponent
978− dragComponentProperties: { "appId": appId }
1035+ dragComponentProperties: { "spreadDelegate": spreadDelegate }
9791036
9801037 onPressed: {
981− function matchDelegate(obj) { return String(obj.objectName).indexOf("tabletSpreadDelegate") >= 0; }
1038+ function matchDelegate(obj) { return String(obj.objectName).indexOf("spreadDelegate") >= 0; }
9821039
9831040 var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
9841041 if (!delegateAtCenter) return;
9851042
986− appId = delegateAtCenter.appId;
1043+ spreadDelegate = delegateAtCenter;
9871044 }
9881045
9891046 onClicked: {
@@ -1003,11 +1060,11 @@ AbstractStage {
10031060
10041061 Component {
10051062 id: dragComponent
1006− SessionContainer {
1007− property string appId: ""
1008− property var application: ApplicationManager.findApplication(appId)
1063+ SurfaceContainer {
1064+ property Item spreadDelegate
1065+
1066+ surface: spreadDelegate ? spreadDelegate.surface : null
10091067
1010− session: application ? application.session : null
10111068 interactive: false
10121069 resizeSurface: false
10131070 focus: false
@@ -1019,10 +1076,11 @@ AbstractStage {
10191076 Drag.hotSpot.y: height/2
10201077 // only accept opposite stage.
10211078 Drag.keys: {
1022− if (!application) return "Disabled";
1079+ if (!surface) return "Disabled";
10231080
1024− if (application.stage === ApplicationInfo.MainStage) {
1025− if (application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1081+ if (spreadDelegate.stage === ApplicationInfo.MainStage) {
1082+ if (spreadDelegate.application.supportedOrientations
1083+ & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
10261084 return "MainStage";
10271085 }
10281086 return "Disabled";
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}
tests/mocks/Unity/Application/ApplicationInfo.cpp+168−110
@@ -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,108 @@
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
2952
3053ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
3154 : ApplicationInfoInterface(appId, parent)
3255 , 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)
4856{
57+ connect(&m_surfaceList, &MirSurfaceListModel::countChanged,
58+ this, &ApplicationInfo::onSurfaceCountChanged, Qt::QueuedConnection);
59+
60+ m_surfaceCreationTimer.setSingleShot(true);
61+ m_surfaceCreationTimer.setInterval(500);
62+ connect(&m_surfaceCreationTimer, &QTimer::timeout, this, &ApplicationInfo::createSurface);
4963}
5064
5165ApplicationInfo::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)
66+ : ApplicationInfo(QString(), parent)
6867{
6968}
7069
7170ApplicationInfo::~ApplicationInfo()
7271{
73− delete m_session;
74−}
75−
76−void ApplicationInfo::createSession()
77−{
78− if (m_session || state() == ApplicationInfo::Stopped) { return; }
79−
80− setSession(SessionManager::singleton()->createSession(appId(), m_screenshotFileName));
8172}
8273
83−void ApplicationInfo::destroySession()
74+void ApplicationInfo::createSurface()
8475{
85− Session *session = this->session();
86− setSession(nullptr);
87− delete session;
88−}
76+ if (state() == ApplicationInfo::Stopped) { return; }
8977
90−void ApplicationInfo::setSession(Session* session)
91−{
92− if (m_session == session)
93− 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();
78+ QString surfaceName = name();
79+ if (m_surfaceList.count() > 0) {
80+ surfaceName.append(QString(" %1").arg(m_surfaceList.count()+1));
10081 }
10182
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);
83+ auto surface = SurfaceManager::singleton()->createSurface(surfaceName,
84+ Mir::NormalType,
85+ fullscreen() ? Mir::FullscreenState : Mir::MaximizedState,
86+ m_screenshotFileName);
87+
88+ surface->setShellChrome(m_shellChrome);
89+
90+ m_surfaceList.appendSurface(surface);
91+
92+ ++m_liveSurfaceCount;
93+ connect(surface, &MirSurface::liveChanged, this, [this, surface](){
94+ if (!surface->live()) {
95+ --m_liveSurfaceCount;
96+ if (m_liveSurfaceCount == 0) {
97+ if (m_closingSurfaces.contains(surface)
98+ || (m_state == Running && m_requestedState == RequestedRunning)) {
99+ Q_EMIT closed();
100+ }
101+ setState(Stopped);
102+ } else {
103+ if (m_closingSurfaces.contains(surface) && m_requestedState == RequestedSuspended
104+ && m_closingSurfaces.count() == 1) {
105+ setState(Suspended);
106+ }
107+ }
108+ m_closingSurfaces.removeAll(surface);
109+ }
110+ });
111+ connect(surface, &MirSurface::focusRequested, this, [this](){
112+ Q_EMIT this->focusRequested();
113+ });
114+ connect(surface, &MirSurface::closeRequested, this, [this, surface](){
115+ m_closingSurfaces.append(surface);
116+ if (m_state == Suspended) {
117+ // resume to allow application to close its surface
118+ setState(Running);
119+ }
120+ });
112121
113− if (!m_manualSurfaceCreation) {
114− QTimer::singleShot(500, m_session, &Session::createSurface);
122+ if (m_state == Starting) {
123+ if (m_requestedState == RequestedRunning) {
124+ setState(Running);
125+ } else {
126+ setState(Suspended);
115127 }
116128 }
117−
118− Q_EMIT sessionChanged(m_session);
119129}
120130
121131void ApplicationInfo::setIconId(const QString &iconId)
@@ -138,10 +148,6 @@ void ApplicationInfo::setScreenshotId(const QString &screenshotId)
138148
139149 if (screenshotFileName != m_screenshotFileName) {
140150 m_screenshotFileName = screenshotFileName;
141−
142− if (m_session) {
143− m_session->setScreenshot(screenshotFileName);
144− }
145151 }
146152}
147153
@@ -172,38 +178,53 @@ void ApplicationInfo::setStage(Stage value)
172178void ApplicationInfo::setState(State value)
173179{
174180 if (value != m_state) {
181+ DEBUG_MSG(qPrintable(stateToStr(value)));
182+ if (!m_manualSurfaceCreation && value == ApplicationInfo::Starting) {
183+ Q_ASSERT(m_surfaceList.count() == 0);
184+ m_surfaceCreationTimer.start();
185+ } else if (value == ApplicationInfo::Stopped) {
186+ m_surfaceCreationTimer.stop();
187+ for (int i = 0; i < m_surfaceList.count(); ++i) {
188+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
189+ surface->setLive(false);
190+ }
191+ }
192+
175193 m_state = value;
176194 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− }
185195 }
186196}
187197
188−void ApplicationInfo::setFocused(bool value)
198+void ApplicationInfo::close()
189199{
190− if (value != m_focused) {
191− m_focused = value;
192− Q_EMIT focusedChanged(value);
200+ DEBUG_MSG("");
201+
202+ if (m_surfaceList.count() > 0) {
203+ for (int i = 0; i < m_surfaceList.count(); ++i) {
204+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
205+ surface->close();
206+ }
207+ } else {
208+ setState(Stopped);
209+ Q_EMIT closed();
193210 }
194211}
195212
196213void ApplicationInfo::setFullscreen(bool value)
197214{
198215 m_fullscreen = value;
199− if (m_session) {
200− m_session->setFullscreen(value);
216+ if (m_surfaceList.rowCount() > 0) {
217+ m_surfaceList.get(0)->setState(Mir::FullscreenState);
201218 }
202219}
203220
204221bool ApplicationInfo::fullscreen() const
205222{
206− return m_session ? m_session->fullscreen() : m_fullscreen;
223+ if (m_surfaceList.rowCount() > 0) {
224+ return m_surfaceList.get(0)->state() == Mir::FullscreenState;
225+ } else {
226+ return m_fullscreen;
227+ }
207228}
208229
209230void ApplicationInfo::setManualSurfaceCreation(bool value)
@@ -241,15 +262,28 @@ ApplicationInfo::RequestedState ApplicationInfo::requestedState() const
241262
242263void ApplicationInfo::setRequestedState(RequestedState value)
243264{
244− if (m_requestedState != value) {
245− m_requestedState = value;
246− Q_EMIT requestedStateChanged(m_requestedState);
265+ if (m_requestedState == value) {
266+ return;
267+ }
268+ DEBUG_MSG((value == RequestedRunning ? "RequestedRunning" : "RequestedSuspended") );
269+
270+ m_requestedState = value;
271+ Q_EMIT requestedStateChanged(m_requestedState);
272+
273+ if (m_requestedState == RequestedRunning) {
247274
248− if (m_requestedState == RequestedRunning && m_state == Suspended) {
275+ if (m_state == Suspended) {
276+ Q_ASSERT(m_liveSurfaceCount > 0);
249277 setState(Running);
250− } else if (m_requestedState == RequestedSuspended && m_state == Running) {
251− setState(Suspended);
278+ } else if (m_state == Stopped) {
279+ Q_ASSERT(m_liveSurfaceCount == 0);
280+ // it's restarting
281+ setState(Starting);
252282 }
283+
284+ } else if (m_requestedState == RequestedSuspended && m_state == Running
285+ && m_closingSurfaces.isEmpty()) {
286+ setState(Suspended);
253287 }
254288}
255289
@@ -263,18 +297,6 @@ void ApplicationInfo::setIsTouchApp(bool isTouchApp)
263297 m_isTouchApp = isTouchApp;
264298}
265299
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−
278300bool ApplicationInfo::exemptFromLifecycle() const
279301{
280302 return m_exemptFromLifecycle;
@@ -305,7 +327,43 @@ void ApplicationInfo::setInitialSurfaceSize(const QSize &size)
305327void ApplicationInfo::setShellChrome(Mir::ShellChrome shellChrome)
306328{
307329 m_shellChrome = shellChrome;
308− if (m_session && m_session->lastSurface()) {
309− m_session->lastSurface()->setShellChrome(shellChrome);
330+ if (m_surfaceList.rowCount() > 0) {
331+ static_cast<MirSurface*>(m_surfaceList.get(0))->setShellChrome(shellChrome);
332+ }
333+}
334+
335+bool ApplicationInfo::focused() const
336+{
337+ bool someSurfaceHasFocus = false; // to be proven wrong
338+ for (int i = 0; i < m_surfaceList.count() && !someSurfaceHasFocus; ++i) {
339+ someSurfaceHasFocus = m_surfaceList.get(i)->focused();
340+ }
341+ return someSurfaceHasFocus;
342+}
343+
344+void ApplicationInfo::setFocused(bool value)
345+{
346+ if (focused() == value) {
347+ return;
348+ }
349+
350+ if (value) {
351+ if (m_surfaceList.count() > 0) {
352+ m_surfaceList.get(0)->requestFocus();
353+ }
354+ } else {
355+ for (int i = 0; i < m_surfaceList.count(); ++i) {
356+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
357+ if (surface->focused()) {
358+ surface->setFocused(false);
359+ }
360+ }
361+ }
362+}
363+
364+void ApplicationInfo::onSurfaceCountChanged()
365+{
366+ if (m_surfaceList.count() == 0 && m_state == Running) {
367+ setState(Stopped);
310368 }
311369}
tests/mocks/Unity/Application/ApplicationInfo.h+40−24
@@ -20,19 +20,24 @@
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
@@ -66,8 +71,7 @@ public:
6671 Q_INVOKABLE void setState(State value);
6772 State state() const override { return m_state; }
6873
69− void setFocused(bool value);
70− bool focused() const override { return m_focused; }
74+ bool focused() const override;
7175
7276 QString splashTitle() const override { return QString(); }
7377 QUrl splashImage() const override { return QUrl(); }
@@ -100,21 +104,29 @@ public:
100104 void setInitialSurfaceSize(const QSize &size) override;
101105
102106 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
103−public:
104− void setSession(Session* session);
105− Session* session() const { return m_session; }
107+
108+ MirSurfaceListInterface* surfaceList() override { return &m_surfaceList; }
109+
110+ void setFocused(bool value);
111+
112+ //////
113+ // internal mock stuff
114+ void close();
106115
107116Q_SIGNALS:
108− void sessionChanged(Session*);
109117 void fullscreenChanged(bool value);
110118 void manualSurfaceCreationChanged(bool value);
119+ void closed();
120+
121+ ////
122+ // FIXME: Move to unity::shell::application::ApplicationInfoInterface
123+ void focusRequested();
111124
112125public Q_SLOTS:
113− Q_INVOKABLE void createSession();
114− Q_INVOKABLE void destroySession();
126+ Q_INVOKABLE void createSurface();
115127
116128private Q_SLOTS:
117− void onSessionSurfaceAdded(MirSurface*);
129+ void onSurfaceCountChanged();
118130
119131private:
120132 void setIcon(const QUrl &value);
@@ -124,20 +136,24 @@ private:
124136 QString m_appId;
125137 QString m_name;
126138 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;
139+ Stage m_stage{MainStage};
140+ State m_state{Stopped};
141+ bool m_fullscreen{false};
142+ Qt::ScreenOrientations m_supportedOrientations{Qt::PortraitOrientation |
143+ Qt::LandscapeOrientation |
144+ Qt::InvertedPortraitOrientation |
145+ Qt::InvertedLandscapeOrientation};
146+ bool m_rotatesWindowContents{false};
147+ RequestedState m_requestedState{RequestedRunning};
148+ bool m_isTouchApp{true};
149+ bool m_exemptFromLifecycle{false};
137150 QSize m_initialSurfaceSize;
138−
139− bool m_manualSurfaceCreation;
140− Mir::ShellChrome m_shellChrome;
151+ MirSurfaceListModel m_surfaceList;
152+ int m_liveSurfaceCount{0};
153+ QTimer m_surfaceCreationTimer;
154+ QList<MirSurface*> m_closingSurfaces;
155+ bool m_manualSurfaceCreation{false};
156+ Mir::ShellChrome m_shellChrome{Mir::NormalChrome};
141157};
142158
143159Q_DECLARE_METATYPE(ApplicationInfo*)
tests/mocks/Unity/Application/ApplicationManager.cpp+104−40
@@ -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,6 +30,16 @@
3130#include <QDateTime>
3231#include <QtDBus/QtDBus>
3332
33+#define APPLICATIONMANAGER_DEBUG 0
34+
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
40+
41+namespace unityapi = unity::shell::application;
42+
3443ApplicationManager *ApplicationManager::the_application_manager = nullptr;
3544
3645ApplicationManager *ApplicationManager::singleton()
@@ -50,9 +59,6 @@ ApplicationManager *ApplicationManager::singleton()
5059ApplicationManager::ApplicationManager(QObject *parent)
5160 : ApplicationManagerInterface(parent)
5261{
53− m_roleNames.insert(RoleSession, "session");
54− m_roleNames.insert(RoleFullscreen, "fullscreen");
55−
5662 buildListOfAvailableApplications();
5763
5864 // polling to find out when the toplevel window has been created as there's
@@ -61,6 +67,9 @@ ApplicationManager::ApplicationManager(QObject *parent)
6167 this, &ApplicationManager::onWindowCreatedTimerTimeout);
6268 m_windowCreatedTimer.setSingleShot(false);
6369 m_windowCreatedTimer.start(200);
70+
71+ connect(MirFocusController::instance(), &MirFocusController::focusedSurfaceChanged,
72+ this, &ApplicationManager::updateFocusedApplication, Qt::QueuedConnection);
6473}
6574
6675ApplicationManager::~ApplicationManager()
@@ -109,10 +118,8 @@ QVariant ApplicationManager::data(const QModelIndex& index, int role) const {
109118 return app->isTouchApp();
110119 case RoleExemptFromLifecycle:
111120 return app->exemptFromLifecycle();
112− case RoleSession:
113− return QVariant::fromValue(app->session());
114− case RoleFullscreen:
115− return app->fullscreen();
121+ case RoleApplication:
122+ return QVariant::fromValue(static_cast<unityapi::ApplicationInfoInterface*>(app));
116123 default:
117124 return QVariant();
118125 }
@@ -144,19 +151,17 @@ QModelIndex ApplicationManager::findIndex(ApplicationInfo* application)
144151 return QModelIndex();
145152}
146153
147−void ApplicationManager::add(ApplicationInfo *application) {
154+bool ApplicationManager::add(ApplicationInfo *application) {
148155 if (!application || m_runningApplications.contains(application)) {
149− return;
156+ return false;
150157 }
158+ DEBUG_MSG(application->appId());
159+
160+ application->setState(ApplicationInfo::Starting);
151161
152162 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
153163 m_runningApplications.append(application);
154164
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− });
160165 connect(application, &ApplicationInfo::focusedChanged, this, [application, this]() {
161166 QModelIndex appIndex = findIndex(application);
162167 if (!appIndex.isValid()) return;
@@ -173,19 +178,27 @@ void ApplicationManager::add(ApplicationInfo *application) {
173178 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleStage);
174179 });
175180
181+ connect(application, &ApplicationInfo::closed, this, [application, this]() {
182+ this->remove(application);
183+ });
184+ connect(application, &ApplicationInfo::focusRequested, this, [application, this]() {
185+ Q_EMIT this->focusRequested(application->appId());
186+ });
187+
176188 endInsertRows();
177− Q_EMIT applicationAdded(application->appId());
178189 Q_EMIT countChanged();
179190 if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
191+
192+ return true;
180193}
181194
182195void ApplicationManager::remove(ApplicationInfo *application) {
183196 int i = m_runningApplications.indexOf(application);
184197 if (i != -1) {
198+ DEBUG_MSG(application->appId());
185199 beginRemoveRows(QModelIndex(), i, i);
186200 m_runningApplications.removeAt(i);
187201 endRemoveRows();
188− Q_EMIT applicationRemoved(application->appId());
189202 Q_EMIT countChanged();
190203 if (isEmpty()) Q_EMIT emptyChanged(isEmpty());
191204 }
@@ -209,18 +222,29 @@ void ApplicationManager::move(int from, int to) {
209222ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
210223 const QStringList &arguments)
211224{
225+ DEBUG_MSG(appId);
212226 Q_UNUSED(arguments)
213− ApplicationInfo *application = add(appId);
227+
228+ ApplicationInfo *application = findApplication(appId);
229+ if (application) {
230+ // the requested app is already running
231+ return application;
232+ } else {
233+ application = add(appId);
234+ }
235+
236+ // most likely not among the available ones
214237 if (!application)
215− return 0;
216− application->setState(ApplicationInfo::Starting);
238+ return nullptr;
239+
240+ Q_EMIT application->focusRequested(); // we assume that an application that's starting up wants focus
217241
218242 return application;
219243}
220244
221245ApplicationInfo* ApplicationManager::add(QString appId)
222246{
223− ApplicationInfo *application = 0;
247+ ApplicationInfo *application = nullptr;
224248
225249 for (ApplicationInfo *availableApp : m_availableApplications) {
226250 if (availableApp->appId() == appId) {
@@ -229,14 +253,18 @@ ApplicationInfo* ApplicationManager::add(QString appId)
229253 }
230254 }
231255
232− if (application)
233− add(application);
256+ if (application) {
257+ if (!add(application)) {
258+ application = nullptr;
259+ }
260+ }
234261
235262 return application;
236263}
237264
238265bool ApplicationManager::stopApplication(const QString &appId)
239266{
267+ DEBUG_MSG(appId);
240268 ApplicationInfo *application = findApplication(appId);
241269 if (application == nullptr)
242270 return false;
@@ -244,8 +272,7 @@ bool ApplicationManager::stopApplication(const QString &appId)
244272 if (application->appId() == focusedApplicationId()) {
245273 unfocusCurrentApplication();
246274 }
247− application->setState(ApplicationInfo::Stopped);
248− remove(application);
275+ application->close();
249276 return true;
250277}
251278
@@ -264,25 +291,23 @@ bool ApplicationManager::focusApplication(const QString &appId)
264291 if (application == nullptr)
265292 return false;
266293
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
275294 application->setFocused(true);
276295
277− // move app to top of stack
278− move(m_runningApplications.indexOf(application), 0);
279− Q_EMIT focusedApplicationIdChanged();
280296 return true;
281297}
282298
283299bool ApplicationManager::requestFocusApplication(const QString &appId)
284300{
285− QMetaObject::invokeMethod(this, "focusRequested", Qt::QueuedConnection, Q_ARG(QString, appId));
301+ ApplicationInfo *application = findApplication(appId);
302+ if (application == nullptr)
303+ return false;
304+
305+ auto surfaceList = static_cast<MirSurfaceListModel*>(application->surfaceList());
306+ if (surfaceList->count() == 0)
307+ return false;
308+
309+ surfaceList->get(0)->requestFocus();
310+
286311 return true;
287312}
288313
@@ -293,7 +318,6 @@ void ApplicationManager::unfocusCurrentApplication()
293318 app->setFocused(false);
294319 }
295320 }
296− Q_EMIT focusedApplicationIdChanged();
297321}
298322
299323void ApplicationManager::buildListOfAvailableApplications()
@@ -473,3 +497,43 @@ bool ApplicationManager::isEmpty() const
473497{
474498 return m_runningApplications.isEmpty();
475499}
500+
501+void ApplicationManager::updateFocusedApplication()
502+{
503+ ApplicationInfo *focusedApplication = nullptr;
504+ ApplicationInfo *previouslyFocusedApplication = nullptr;
505+
506+ MirSurface *surface = static_cast<MirSurface*>(MirFocusController::instance()->focusedSurface());
507+ if (surface) {
508+ focusedApplication = findApplication(surface);
509+ }
510+
511+ surface = static_cast<MirSurface*>(MirFocusController::instance()->previouslyFocusedSurface());
512+ if (surface) {
513+ previouslyFocusedApplication = findApplication(surface);
514+ }
515+
516+ if (focusedApplication != previouslyFocusedApplication) {
517+ if (focusedApplication) {
518+ DEBUG_MSG("focused " << focusedApplication->appId());
519+ Q_EMIT focusedApplication->focusedChanged(true);
520+ this->move(this->m_runningApplications.indexOf(focusedApplication), 0);
521+ }
522+ if (previouslyFocusedApplication) {
523+ DEBUG_MSG("unfocused " << previouslyFocusedApplication->appId());
524+ Q_EMIT previouslyFocusedApplication->focusedChanged(false);
525+ }
526+ Q_EMIT focusedApplicationIdChanged();
527+ }
528+}
529+
530+ApplicationInfo *ApplicationManager::findApplication(MirSurface* surface)
531+{
532+ for (ApplicationInfo *app : m_runningApplications) {
533+ auto surfaceList = static_cast<MirSurfaceListModel*>(app->surfaceList());
534+ if (surfaceList->contains(surface)) {
535+ return app;
536+ }
537+ }
538+ return nullptr;
539+}
tests/mocks/Unity/Application/ApplicationManager.h+12−8
@@ -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)
@@ -42,11 +49,6 @@ class ApplicationManager : public ApplicationManagerInterface {
4249
4350 static ApplicationManager *singleton();
4451
45− enum MoreRoles {
46− RoleSession = RoleExemptFromLifecycle+1,
47− RoleFullscreen,
48− };
49−
5052 // QAbstractItemModel methods.
5153 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
5254 QVariant data(const QModelIndex& index, int role) const override;
@@ -79,12 +81,14 @@ class ApplicationManager : public ApplicationManagerInterface {
7981
8082 private Q_SLOTS:
8183 void onWindowCreatedTimerTimeout();
84+ void updateFocusedApplication();
8285
8386 private:
84− void add(ApplicationInfo *application);
87+ bool add(ApplicationInfo *application);
8588 void remove(ApplicationInfo* application);
8689 void buildListOfAvailableApplications();
8790 void onWindowCreated();
91+ ApplicationInfo *findApplication(MirSurface* surface);
8892 QList<ApplicationInfo*> m_runningApplications;
8993 QList<ApplicationInfo*> m_availableApplications;
9094 QTimer m_windowCreatedTimer;
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+128−7
@@ -19,6 +19,18 @@
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+#else
27+#define DEBUG_MSG(params) ((void)0)
28+#endif
29+
30+using namespace unity::shell::application;
31+
32+MirFocusController *MirFocusController::m_instance = nullptr;
33+
2234MirSurface::MirSurface(const QString& name,
2335 Mir::Type type,
2436 Mir::State state,
@@ -40,17 +52,27 @@ MirSurface::MirSurface(const QString& name,
4052 , m_slowToResize(false)
4153 , m_shellChrome(Mir::NormalChrome)
4254{
43−// qDebug() << "MirSurface::MirSurface() " << name;
55+ DEBUG_MSG("");
56+
4457 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
4558
4659 m_delayedResizeTimer.setInterval(600);
4760 m_delayedResizeTimer.setSingleShot(true);
4861 connect(&m_delayedResizeTimer, &QTimer::timeout, this, &MirSurface::applyDelayedResize);
62+
63+ m_zombieTimer.setInterval(100);
64+ m_zombieTimer.setSingleShot(true);
65+ connect(&m_zombieTimer, &QTimer::timeout, this, [this](){ this->setLive(false); });
4966}
5067
5168MirSurface::~MirSurface()
5269{
53−// qDebug() << "MirSurface::~MirSurface() " << name();
70+ DEBUG_MSG("");
71+
72+ auto controller = MirFocusController::instance();
73+ if (controller->focusedSurface() == this) {
74+ controller->clear();
75+ }
5476}
5577
5678QString MirSurface::name() const
@@ -89,10 +111,10 @@ bool MirSurface::visible() const
89111
90112void MirSurface::setLive(bool live)
91113{
92−// qDebug().nospace() << "MirSurface::setLive("<<live<<") " << name();
93114 if (live == m_live)
94115 return;
95116
117+ DEBUG_MSG(live);
96118 m_live = live;
97119 Q_EMIT liveChanged(live);
98120
@@ -164,15 +186,13 @@ void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
164186void MirSurface::registerView(qintptr viewId)
165187{
166188 m_views.insert(viewId, MirSurface::View{false});
167−// qDebug().nospace() << "MirSurface[" << name() << "]::registerView(" << viewId << ")"
168−// << " after=" << m_views.count();
189+ DEBUG_MSG(viewId << " after=" << m_views.count());
169190}
170191
171192void MirSurface::unregisterView(qintptr viewId)
172193{
173−// qDebug().nospace() << "MirSurface[" << name() << "]::unregisterView(" << viewId << ")"
174−// << " after=" << m_views.count() << " live=" << m_live;
175194 m_views.remove(viewId);
195+ DEBUG_MSG(viewId << " after=" << m_views.count() << " live=" << m_live);
176196 if (!m_live && m_views.count() == 0) {
177197 deleteLater();
178198 }
@@ -215,6 +235,9 @@ void MirSurface::setActiveFocus(bool value)
215235 return;
216236
217237 m_activeFocus = value;
238+ if (value) {
239+ MirFocusController::instance()->setFocusedSurface(this);
240+ }
218241
219242 Q_EMIT activeFocusChanged(value);
220243}
@@ -288,6 +311,7 @@ bool MirSurface::isSlowToResize() const
288311void MirSurface::setSlowToResize(bool value)
289312{
290313 if (m_slowToResize != value) {
314+ DEBUG_MSG(value);
291315 m_slowToResize = value;
292316 Q_EMIT slowToResizeChanged();
293317 if (!m_slowToResize && m_delayedResizeTimer.isActive()) {
@@ -344,3 +368,100 @@ void MirSurface::setHeightIncrement(int value)
344368 Q_EMIT heightIncrementChanged(m_heightIncrement);
345369 }
346370}
371+
372+void MirSurface::raise()
373+{
374+ Q_EMIT raiseRequested();
375+}
376+
377+void MirSurface::close()
378+{
379+ DEBUG_MSG("");
380+ m_zombieTimer.start();
381+ Q_EMIT closeRequested();
382+}
383+
384+void MirSurface::createPromptSurface()
385+{
386+ QStringList screenshotIds = {"gallery", "map", "facebook", "camera", "browser", "music", "twitter"};
387+ int i = rand() % screenshotIds.count();
388+
389+ QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%1@12.png")
390+ .arg(screenshotIds[i]);
391+
392+ auto surface = new MirSurface(QString("prompt foo"),
393+ Mir::NormalType,
394+ Mir::RestoredState,
395+ screenshotUrl);
396+
397+ m_promptSurfaceList.appendSurface(surface);
398+}
399+
400+MirSurfaceListInterface* MirSurface::promptSurfaceList()
401+{
402+ return &m_promptSurfaceList;
403+}
404+
405+void MirSurface::requestFocus()
406+{
407+ DEBUG_MSG("");
408+ Q_EMIT focusRequested();
409+}
410+
411+void MirSurface::setFocused(bool value)
412+{
413+ DEBUG_MSG(value);
414+ auto controller = MirFocusController::instance();
415+ if (value) {
416+ controller->setFocusedSurface(this);
417+ } else if (controller->focusedSurface() == this) {
418+ controller->setFocusedSurface(nullptr);
419+ }
420+}
421+
422+bool MirSurface::focused() const
423+{
424+ return MirFocusController::instance()->focusedSurface() == this;
425+}
426+
427+#if MIRSURFACE_DEBUG
428+#undef DEBUG_MSG
429+#define DEBUG_MSG(params) qDebug().nospace() << "MirFocusController::" << __func__ << " " << params
430+#endif
431+
432+void MirFocusController::setFocusedSurface(MirSurfaceInterface *surface)
433+{
434+ if (m_focusedSurface == surface) {
435+ return;
436+ }
437+ DEBUG_MSG("MirSurface[" << (void*)surface << "," << (surface?surface->name():"") << "]");
438+
439+ m_previouslyFocusedSurface = m_focusedSurface;
440+ m_focusedSurface = surface;
441+
442+ if (m_previouslyFocusedSurface != m_focusedSurface) {
443+ Q_EMIT focusedSurfaceChanged();
444+ }
445+
446+ if (m_previouslyFocusedSurface) {
447+ Q_EMIT m_previouslyFocusedSurface->focusedChanged(false);
448+ }
449+
450+ if (m_focusedSurface) {
451+ Q_EMIT m_focusedSurface->focusedChanged(true);
452+ }
453+}
454+
455+MirFocusController* MirFocusController::instance()
456+{
457+ if (!m_instance) {
458+ m_instance = new MirFocusController;
459+ }
460+ return m_instance;
461+}
462+
463+void MirFocusController::clear()
464+{
465+ m_focusedSurface = m_previouslyFocusedSurface = nullptr;
466+ Q_EMIT focusedSurfaceChanged();
467+}
tests/mocks/Unity/Application/MirSurface.h+49−5
@@ -23,8 +23,30 @@
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+ static MirFocusController* instance();
39+
40+ void setFocusedSurface(unity::shell::application::MirSurfaceInterface *surface) override;
41+ unity::shell::application::MirSurfaceInterface* focusedSurface() const override { return m_focusedSurface; }
42+ unity::shell::application::MirSurfaceInterface* previouslyFocusedSurface() { return m_previouslyFocusedSurface; }
43+ void clear();
44+private:
45+ static MirFocusController *m_instance;
46+ unity::shell::application::MirSurfaceInterface* m_previouslyFocusedSurface{nullptr};
47+ unity::shell::application::MirSurfaceInterface* m_focusedSurface{nullptr};
48+};
49+
2850class MirSurface : public unity::shell::application::MirSurfaceInterface
2951{
3052 Q_OBJECT
@@ -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+5−4
@@ -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,6 +16,7 @@
1616
1717#include "SurfaceManager.h"
1818
19+#include "ApplicationInfo.h"
1920#include "VirtualKeyboard.h"
2021
2122#include <paths.h>
@@ -44,9 +45,9 @@ SurfaceManager::SurfaceManager(QObject *parent) :
4445}
4546
4647MirSurface *SurfaceManager::createSurface(const QString& name,
47− Mir::Type type,
48− Mir::State state,
49− const QUrl& screenshot)
48+ Mir::Type type,
49+ Mir::State state,
50+ const QUrl& screenshot)
5051{
5152 MirSurface* surface = new MirSurface(name, type, state, screenshot);
5253 connect(surface, &QObject::destroyed, this, [this](QObject *obj) {
tests/mocks/Unity/Application/SurfaceManager.h+2
@@ -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
tests/mocks/Unity/Application/plugin.cpp+5−27
@@ -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
@@ -42,50 +39,34 @@ static QObject* surfaceManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEng
4239 return SurfaceManager::singleton();
4340}
4441
45−static QObject* sessionManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
46− Q_UNUSED(engine);
47− Q_UNUSED(scriptEngine);
48− return SessionManager::singleton();
49−}
50−
5142static QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*) {
5243 return UbuntuKeyboardInfo::singleton();
5344}
5445
55−
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;
46+static QObject* mirFocusControllerSingleton(QQmlEngine*, QJSEngine*)
47+{
48+ return MirFocusController::instance();
6549}
6650
6751void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
6852{
69− qRegisterMetaType<Session*>("Session*");
7053 qRegisterMetaType<ApplicationInfo*>("ApplicationInfo*");
71− qRegisterMetaType<SessionModel*>("SessionModel*");
7254 qRegisterMetaType<unity::shell::application::MirSurfaceInterface*>("unity::shell::application::MirSurfaceInterface*");
55+ qRegisterMetaType<unity::shell::application::MirSurfaceListInterface*>("unity::shell::application::MirSurfaceListInterface*");
7356 qRegisterMetaType<Mir::Type>("Mir::Type");
7457 qRegisterMetaType<Mir::State>("Mir::State");
7558
7659 qmlRegisterUncreatableType<unity::shell::application::ApplicationManagerInterface>(uri, 0, 1, "ApplicationManagerInterface", "Abstract interface. Cannot be created in QML");
7760 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");
7961 qmlRegisterUncreatableType<MirSurface>(uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
8062 qmlRegisterUncreatableType<unity::shell::application::MirSurfaceInterface>(
8163 uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
64+ qmlRegisterSingletonType<MirFocusController>(uri, 0, 1, "MirFocusController", mirFocusControllerSingleton);
8265 qmlRegisterType<MirSurfaceItem>(uri, 0, 1, "MirSurfaceItem");
8366 qmlRegisterType<ApplicationInfo>(uri, 0, 1, "ApplicationInfo");
8467
8568 qmlRegisterSingletonType<ApplicationManager>(uri, 0, 1, "ApplicationManager", applicationManagerSingleton);
8669 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
87− qmlRegisterSingletonType<SessionManager>(uri, 0, 1, "SessionManager", sessionManagerSingleton);
88− qmlRegisterSingletonType<ApplicationTestInterface>(uri, 0, 1, "ApplicationTest", applicationTestInterface);
8970 qmlRegisterSingletonType<UbuntuKeyboardInfo>(uri, 0, 1, "UbuntuKeyboardInfo", ubuntuKeyboardInfoSingleton);
9071
9172 qmlRegisterUncreatableType<Mir>(uri, 0, 1, "Mir", "Mir provides enum values, it can't be instantiated");
@@ -94,7 +75,4 @@ void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
9475void FakeUnityApplicationQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
9576{
9677 QQmlExtensionPlugin::initializeEngine(engine, uri);
97−
98− // make sure we initialise our test interface.
99− applicationTestInterface(engine, nullptr);
10078}
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+1
@@ -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); }
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/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+228−159
@@ -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
@@ -53,6 +53,11 @@ Item {
5353 }
5454 }
5555
56+ TopLevelSurfaceList {
57+ id: topSurfaceList
58+ applicationsModel: ApplicationManager
59+ }
60+
5661 Loader {
5762 id: desktopStageLoader
5863 x: ((root.width - controls.width) - width) / 2
@@ -68,6 +73,7 @@ Item {
6873 color: "white"
6974 anchors.fill: parent
7075 background: "../../../qml/graphics/tablet_background.jpg"
76+ focus: true
7177
7278 Component.onCompleted: {
7379 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
@@ -76,6 +82,8 @@ Item {
7682 desktopStageLoader.itemDestroyed = true;
7783 }
7884 orientations: Orientations {}
85+ applicationManager: ApplicationManager
86+ topLevelSurfaceList: topSurfaceList
7987 }
8088 }
8189 }
@@ -103,7 +111,7 @@ Item {
103111 activeFocusOnPress: false
104112 onClicked: {
105113 if (ApplicationManager.focusedApplicationId) {
106− var surface = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).session.lastSurface;
114+ var surface = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).surfaceList.get(0);
107115 surface.slowToResize = true;
108116 }
109117 }
@@ -137,6 +145,19 @@ Item {
137145
138146 property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
139147
148+ function init() {
149+ // wait until unity8-dash is up and running.
150+ // it's started automatically by ApplicationManager mock implementation
151+ tryCompare(ApplicationManager, "count", 1);
152+ var dashApp = ApplicationManager.findApplication("unity8-dash");
153+ verify(dashApp);
154+ tryCompare(dashApp, "state", ApplicationInfoInterface.Running);
155+
156+ tryCompare(topSurfaceList, "count", 1);
157+ tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) != null;}, true);
158+ compare(MirFocusController.focusedSurface, topSurfaceList.surfaceAt(0));
159+ }
160+
140161 function cleanup() {
141162 desktopStageLoader.itemDestroyed = false;
142163 desktopStageLoader.active = false;
@@ -149,43 +170,71 @@ Item {
149170 // Shell instance gets destroyed.
150171 tryCompare(desktopStageLoader, "itemDestroyed", true);
151172
152− killAllRunningApps();
173+ killApps();
174+
175+ root.resetGeometry();
153176
154177 desktopStageLoader.active = true;
155178 tryCompare(desktopStageLoader, "status", Loader.Ready);
156− root.resetGeometry();
157− }
158179
159− function killAllRunningApps() {
160− while (ApplicationManager.count > 0) {
161− ApplicationManager.stopApplication(ApplicationManager.get(0).appId);
162− }
163− compare(ApplicationManager.count, 0)
164180 }
165181
166− function waitUntilAppSurfaceShowsUp(appId) {
167− var appWindow = findChild(desktopStage, "appWindow_" + appId);
182+ function waitUntilAppSurfaceShowsUp(surfaceId) {
183+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
184+ verify(appDelegate);
185+ var appWindow = findChild(appDelegate, "appWindow");
168186 verify(appWindow);
169187 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
170188 verify(appWindowStates);
171189 tryCompare(appWindowStates, "state", "surface");
172190 }
173191
192+ /*
193+ Returns the appDelegate of the first surface created by the app with the specified appId
194+ */
174195 function startApplication(appId) {
175− var app = ApplicationManager.findApplication(appId);
176− if (!app) {
196+ try {
197+ var app = ApplicationManager.findApplication(appId);
198+ if (app) {
199+ for (var i = 0; i < topSurfaceList.count; i++) {
200+ if (topSurfaceList.applicationAt(i).appId === appId) {
201+ var appRepeater = findChild(desktopStage, "appRepeater");
202+ verify(appRepeater);
203+ return appRepeater.itemAt(i);
204+ }
205+ }
206+ }
207+
208+ var surfaceId = topSurfaceList.nextId;
177209 app = ApplicationManager.startApplication(appId);
210+ verify(app);
211+ waitUntilAppSurfaceShowsUp(surfaceId);
212+ compare(app.surfaceList.count, 1);
213+
214+ return findChild(desktopStage, "appDelegate_" + surfaceId);
215+ } catch(err) {
216+ throw new Error("startApplication("+appId+") called from line " + util.callerLine(1) + " failed!");
217+ }
218+ }
219+
220+ function maximizeAppDelegate(appDelegate) {
221+ var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
222+ if (!maximizeButton) {
223+ fail("Could not find maximizeWindowButton of appDelegate=" + appDelegate);
224+ }
225+ mouseClick(maximizeButton);
226+
227+ try {
228+ tryCompare(appDelegate, "visuallyMaximized", true);
229+ } catch(err) {
230+ fail("appDelegate.visuallyMaximized !== true");
178231 }
179− verify(app);
180− waitUntilAppSurfaceShowsUp(appId);
181− verify(app.session.lastSurface);
182− return app;
183232 }
184233
185234 function test_appFocusSwitch_data() {
186235 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 },
236+ {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "gmail-webapp" ], focusfrom: 0, focusTo: 1 },
237+ {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "gmail-webapp" ], focusfrom: 1, focusTo: 0 },
189238 ]
190239 }
191240
@@ -193,10 +242,10 @@ Item {
193242 data.apps.forEach(startApplication);
194243
195244 ApplicationManager.requestFocusApplication(data.apps[data.focusfrom]);
196− tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.lastSurface, "activeFocus", true);
245+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).surfaceList.get(0), "activeFocus", true);
197246
198247 ApplicationManager.requestFocusApplication(data.apps[data.focusTo]);
199− tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.lastSurface, "activeFocus", true);
248+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).surfaceList.get(0), "activeFocus", true);
200249 }
201250
202251 function test_tappingOnWindowChangesFocusedApp_data() {
@@ -207,21 +256,24 @@ Item {
207256 }
208257
209258 function test_tappingOnWindowChangesFocusedApp(data) {
210− data.apps.forEach(startApplication);
211− var fromAppId = data.apps[data.focusfrom];
212− var toAppId = data.apps[data.focusTo]
259+ var appDelegates = [];
260+ for (var i = 0; i < data.apps.length; i++) {
261+ appDelegates[i] = startApplication(data.apps[i]);
262+ }
263+ var fromDelegate = appDelegates[data.focusfrom];
264+ var toDelegate = appDelegates[data.focusTo];
213265
214− var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
266+ var fromAppWindow = findChild(fromDelegate, "appWindow");
215267 verify(fromAppWindow);
216268 tap(fromAppWindow);
217− compare(fromAppWindow.application.session.lastSurface.activeFocus, true);
218− compare(ApplicationManager.focusedApplicationId, fromAppId);
269+ compare(fromDelegate.surface.activeFocus, true);
270+ compare(MirFocusController.focusedSurface, fromDelegate.surface);
219271
220− var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
272+ var toAppWindow = findChild(toDelegate, "appWindow");
221273 verify(toAppWindow);
222274 tap(toAppWindow);
223− compare(toAppWindow.application.session.lastSurface.activeFocus, true);
224− compare(ApplicationManager.focusedApplicationId, toAppId);
275+ compare(toDelegate.surface.activeFocus, true);
276+ compare(MirFocusController.focusedSurface, toDelegate.surface);
225277 }
226278
227279 function test_clickingOnWindowChangesFocusedApp_data() {
@@ -229,21 +281,24 @@ Item {
229281 }
230282
231283 function test_clickingOnWindowChangesFocusedApp(data) {
232− data.apps.forEach(startApplication);
233− var fromAppId = data.apps[data.focusfrom];
234− var toAppId = data.apps[data.focusTo]
284+ var appDelegates = [];
285+ for (var i = 0; i < data.apps.length; i++) {
286+ appDelegates[i] = startApplication(data.apps[i]);
287+ }
288+ var fromDelegate = appDelegates[data.focusfrom];
289+ var toDelegate = appDelegates[data.focusTo];
235290
236− var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
291+ var fromAppWindow = findChild(fromDelegate, "appWindow");
237292 verify(fromAppWindow);
238293 mouseClick(fromAppWindow);
239− compare(fromAppWindow.application.session.lastSurface.activeFocus, true);
240− compare(ApplicationManager.focusedApplicationId, fromAppId);
294+ compare(fromDelegate.surface.activeFocus, true);
295+ compare(MirFocusController.focusedSurface, fromDelegate.surface);
241296
242− var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
297+ var toAppWindow = findChild(toDelegate, "appWindow");
243298 verify(toAppWindow);
244299 mouseClick(toAppWindow);
245− compare(toAppWindow.application.session.lastSurface.activeFocus, true);
246− compare(ApplicationManager.focusedApplicationId, toAppId);
300+ compare(toDelegate.surface.activeFocus, true);
301+ compare(MirFocusController.focusedSurface, toDelegate.surface);
247302 }
248303
249304 function test_tappingOnDecorationFocusesApplication_data() {
@@ -253,24 +308,49 @@ Item {
253308 ]
254309 }
255310
311+ function findDecoratedWindow(surfaceId) {
312+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
313+ if (!appDelegate) {
314+ console.warn("Could not find appDelegate for surfaceId="+surfaceId);
315+ return null;
316+ }
317+ return findChild(appDelegate, "decoratedWindow");
318+ }
319+
320+ function findWindowDecoration(surfaceId) {
321+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
322+ if (!appDelegate) {
323+ console.warn("Could not find appDelegate for surfaceId="+surfaceId);
324+ return null;
325+ }
326+ return findChild(appDelegate, "appWindowDecoration");
327+ }
328+
256329 function test_tappingOnDecorationFocusesApplication(data) {
257− data.apps.forEach(startApplication);
330+ var appDelegates = [];
331+ for (var i = 0; i < data.apps.length; i++) {
332+ appDelegates[i] = startApplication(data.apps[i]);
333+ }
334+
335+ var fromDelegate = appDelegates[data.focusfrom];
336+ var toDelegate = appDelegates[data.focusTo];
258337
259− var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
338+ var fromAppDecoration = findChild(fromDelegate, "appWindowDecoration");
260339 verify(fromAppDecoration);
261340 tap(fromAppDecoration);
262341
263− var fromApp = ApplicationManager.findApplication(data.apps[data.focusfrom]);
264− verify(fromApp);
265− tryCompare(fromApp.session.lastSurface, "activeFocus", true);
342+ tryCompare(fromDelegate.surface, "activeFocus", true);
266343
267− var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
344+ var toAppDecoration = findChild(toDelegate, "appWindowDecoration");
268345 verify(toAppDecoration);
346+
347+ // FIXME: Wait a bit before the second tap or the window decoration will say it got double clicked.
348+ // No idea why.
349+ wait(700);
350+
269351 tap(toAppDecoration);
270352
271− var toApp = ApplicationManager.findApplication(data.apps[data.focusTo]);
272− verify(toApp);
273− tryCompare(toApp.session.lastSurface, "activeFocus", true);
353+ tryCompare(toDelegate.surface, "activeFocus", true);
274354 }
275355
276356 function test_clickingOnDecorationFocusesApplication_data() {
@@ -278,159 +358,158 @@ Item {
278358 }
279359
280360 function test_clickingOnDecorationFocusesApplication(data) {
281− data.apps.forEach(startApplication);
361+ var appDelegates = [];
362+ for (var i = 0; i < data.apps.length; i++) {
363+ appDelegates[i] = startApplication(data.apps[i]);
364+ }
365+ var fromDelegate = appDelegates[data.focusfrom];
366+ var toDelegate = appDelegates[data.focusTo];
282367
283− var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
368+ var fromAppDecoration = findChild(fromDelegate, "appWindowDecoration");
284369 verify(fromAppDecoration);
285370 mouseClick(fromAppDecoration);
286− tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.lastSurface, "activeFocus", true);
371+ tryCompare(fromDelegate.surface, "activeFocus", true);
287372
288− var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
373+ var toAppDecoration = findChild(toDelegate, "appWindowDecoration");
289374 verify(toAppDecoration);
290375 mouseClick(toAppDecoration);
291− tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.lastSurface, "activeFocus", true);
376+ tryCompare(toDelegate.surface, "activeFocus", true);
292377 }
293378
294379 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);
380+ var dialerDelegate = startApplication("dialer-app");
381+ startApplication("camera-app");
382+
383+ ApplicationManager.focusApplication("dialer-app");
301384 keyClick(Qt.Key_Up, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Up shortcut to maximize
302− tryCompare(appDelegate, "maximized", true);
303− tryCompare(appDelegate, "minimized", false);
385+ tryCompare(dialerDelegate, "maximized", true);
386+ tryCompare(dialerDelegate, "minimized", false);
304387 }
305388
306389 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);
390+ var dialerDelegate = startApplication("dialer-app");
391+ startApplication("camera-app");
392+
393+ ApplicationManager.focusApplication("dialer-app");
313394 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);
395+ tryCompare(dialerDelegate, "maximized", false);
396+ tryCompare(dialerDelegate, "minimized", false);
397+ tryCompare(dialerDelegate, "maximizedLeft", true);
398+ tryCompare(dialerDelegate, "maximizedRight", false);
318399 }
319400
320401 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);
402+ var dialerDelegate = startApplication("dialer-app");
403+ startApplication("camera-app");
404+
405+ ApplicationManager.focusApplication("dialer-app");
327406 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);
407+ tryCompare(dialerDelegate, "maximized", false);
408+ tryCompare(dialerDelegate, "minimized", false);
409+ tryCompare(dialerDelegate, "maximizedLeft", false);
410+ tryCompare(dialerDelegate, "maximizedRight", true);
332411 }
333412
334413 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);
414+ var dialerDelegate = startApplication("dialer-app");
415+ startApplication("camera-app");
416+
417+ ApplicationManager.focusApplication("dialer-app");
341418 keyClick(Qt.Key_Down, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Down shortcut to minimize
342− tryCompare(appDelegate, "maximized", false);
343− tryCompare(appDelegate, "minimized", true);
419+ tryCompare(dialerDelegate, "maximized", false);
420+ tryCompare(dialerDelegate, "minimized", true);
344421 verify(ApplicationManager.focusedApplicationId != ""); // verify we don't lose focus when minimizing an app
345422 }
346423
347424 function test_windowMinimizeAll() {
348425 var apps = ["unity8-dash", "dialer-app", "camera-app"];
349426 apps.forEach(startApplication);
350− verify(ApplicationManager.count == 3);
427+ verify(topSurfaceList.count == 3);
351428 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
352429 tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
353430 }
354431
355432 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);
433+ var dialerSurfaceId = topSurfaceList.nextId;
434+ var dialerDelegate = startApplication("dialer-app");
435+ verify(topSurfaceList.indexForId(dialerSurfaceId) !== -1);
436+ startApplication("gmail-webapp");
437+ verify(topSurfaceList.count == 3);
438+
439+ mouseClick(dialerDelegate);
440+
441+ var desktopStagePriv = findInvisibleChild(desktopStage, "DesktopStagePrivate");
442+ tryCompare(desktopStagePriv, "focusedAppDelegate", dialerDelegate);
443+
363444 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
445+
446+ // verify the surface is gone
447+ tryCompare(topSurfaceList, "count", 2);
448+ verify(topSurfaceList.indexForId(dialerSurfaceId) === -1);
366449 }
367450
368451 function test_smashCursorKeys() {
369− var apps = ["unity8-dash", "dialer-app", "camera-app"];
452+ var apps = ["dialer-app", "gmail-webapp"];
370453 apps.forEach(startApplication);
371− verify(ApplicationManager.count == 3);
454+ verify(topSurfaceList.count == 3);
372455 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
373− tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
456+ tryCompare(MirFocusController, "focusedSurface", null); // verify no surface is focused
374457
375458 // now try pressing all 4 arrow keys + ctrl + meta
376459 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
460+ tryCompare(MirFocusController, "focusedSurface", null); // verify still no surface is focused
378461 }
379462
380463 function test_minimizeApplicationHidesSurface() {
381− var dashApp = startApplication("unity8-dash");
464+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
465+ var dashSurface = topSurfaceList.surfaceAt(0);
466+ var dashSurfaceId = topSurfaceList.idAt(0);
382467
383− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
384− verify(dashDelegate);
468+ var decoratedWindow = findDecoratedWindow(dashSurfaceId);
469+ verify(decoratedWindow);
385470
386− findChild(dashDelegate, "decoratedWindow").minimize();
387− tryCompare(dashApp.session.lastSurface, "visible", false);
471+ tryCompare(dashSurface, "visible", true);
472+ decoratedWindow.minimize();
473+ tryCompare(dashSurface, "visible", false);
388474 }
389475
390476 function test_maximizeApplicationHidesSurfacesBehindIt() {
391− var dashApp = startApplication("unity8-dash");
392− var dialerApp = startApplication("dialer-app");
393− var gmailApp = startApplication("gmail-webapp");
477+ var dashDelegate = startApplication("unity8-dash");
478+ var dialerDelegate = startApplication("dialer-app");
479+ var gmailDelegate = startApplication("gmail-webapp");
394480
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
481+ // maximize without raising
403482 dialerDelegate.maximize();
404483 tryCompare(dialerDelegate, "visuallyMaximized", true);
405484
406− tryCompare(dashApp.session.lastSurface, "visible", false);
407− compare(gmailApp.session.lastSurface.visible, true);
485+ tryCompare(dashDelegate.surface, "visible", false);
486+ compare(gmailDelegate.surface.visible, true);
408487
409− // restore
488+ // restore without raising
410489 dialerDelegate.restoreFromMaximized();
411− compare(dashApp.session.lastSurface.visible, true);
412− compare(gmailApp.session.lastSurface.visible, true);
490+ compare(dashDelegate.surface.visible, true);
491+ compare(gmailDelegate.surface.visible, true);
413492 }
414493
415494 function test_applicationsBecomeVisibleWhenOccludingAppRemoved() {
416− var dashApp = startApplication("unity8-dash");
417− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
418− verify(dashDelegate);
495+ var dashApp = topSurfaceList.applicationAt(0);
419496
420− var dialerApp = startApplication("dialer-app");
421− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
497+ var dialerSurfaceId = topSurfaceList.nextId;
498+ var dialerDelegate = startApplication("dialer-app");
422499 verify(dialerDelegate);
500+ var dialerApp = dialerDelegate.application;
423501
424502 var dialerMaximizeButton = findChild(dialerDelegate, "maximizeWindowButton");
425503 verify(dialerMaximizeButton);
426504 mouseClick(dialerMaximizeButton);
427505
428− var mapApp = startApplication("map");
429− var mapDelegate = findChild(desktopStage, "appDelegate_map");
506+ var mapSurfaceId = topSurfaceList.nextId;
507+ var mapDelegate = startApplication("map");
430508 verify(mapDelegate);
509+ var mapApp = mapDelegate.application;
431510
432− var gmailApp = startApplication("gmail-webapp");
433− var gmailDelegate = findChild(desktopStage, "appDelegate_gmail-webapp");
511+ var gmailSurfaceId = topSurfaceList.nextId;
512+ var gmailDelegate = startApplication("gmail-webapp");
434513 verify(gmailDelegate);
435514
436515 var gmailMaximizeButton = findChild(gmailDelegate, "maximizeWindowButton");
@@ -440,29 +519,27 @@ Item {
440519 tryCompare(dialerDelegate, "visuallyMaximized", true);
441520 tryCompare(gmailDelegate, "visuallyMaximized", true);
442521
443− tryCompare(dashApp.session.lastSurface, "visible", false);
444− tryCompare(dialerApp.session.lastSurface, "visible", false);
445− tryCompare(mapApp.session.lastSurface, "visible", false);
522+ tryCompare(dashApp.surfaceList.get(0), "visible", false);
523+ tryCompare(dialerApp.surfaceList.get(0), "visible", false);
524+ tryCompare(mapApp.surfaceList.get(0), "visible", false);
446525
447526 ApplicationManager.stopApplication("gmail-webapp");
448527
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
528+ tryCompare(mapApp.surfaceList.get(0), "visible", true);
529+ tryCompare(dialerApp.surfaceList.get(0), "visible", true);
530+ tryCompare(dashApp.surfaceList.get(0), "visible", false); // still occluded by maximised dialer
452531 }
453532
454533 function test_maximisedAppStaysVisibleWhenAppStarts() {
455− var dashApp = startApplication("unity8-dash");
456− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
457− verify(dashDelegate);
534+ var dashDelegate = startApplication("unity8-dash");
535+
458536 // maximize
459537 var dashMaximizeButton = findChild(dashDelegate, "maximizeWindowButton");
460538 verify(dashMaximizeButton);
461539 mouseClick(dashMaximizeButton);
462540 tryCompare(dashDelegate, "visuallyMaximized", true);
463541
464− var dialerApp = startApplication("dialer-app");
465− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
542+ var dialerDelegate = startApplication("dialer-app");
466543 verify(dialerDelegate);
467544
468545 compare(dialerDelegate.visible, true, "Dialer should be visible");
@@ -470,14 +547,11 @@ Item {
470547 }
471548
472549 function test_occlusionWithMultipleMaximized() {
473− var dashApp = startApplication("unity8-dash");
474− var dashAppDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
550+ var dashAppDelegate = startApplication("unity8-dash");
475551
476− var dialerApp = startApplication("dialer-app");
477− var dialerAppDelegate = findChild(desktopStage, "appDelegate_dialer-app");
552+ var dialerAppDelegate = startApplication("dialer-app");
478553
479− var facebookApp = startApplication("facebook-webapp");
480− var facebookAppDelegate = findChild(desktopStage, "appDelegate_facebook-webapp");
554+ var facebookAppDelegate = startApplication("facebook-webapp");
481555
482556 // all of them are in restored state now. all should be visible
483557 tryCompare(dashAppDelegate, "visible", true);
@@ -510,20 +584,15 @@ Item {
510584 }
511585
512586 function test_dropShadow() {
513− // verify the drop shadow is not visible initially
514− verify(PanelState.dropShadow == false);
515−
516587 // start an app, maximize it
517− var facebookApp = startApplication("facebook-webapp");
518− var facebookAppDelegate = findChild(desktopStage, "appDelegate_facebook-webapp");
588+ var facebookAppDelegate = startApplication("facebook-webapp");
519589 facebookAppDelegate.maximize();
520590
521591 // verify the drop shadow is still not visible
522592 verify(PanelState.dropShadow == false);
523593
524594 // start a foreground app, not maximized
525− var dialerApp = startApplication("dialer-app");
526− var dialerAppDelegate = findChild(desktopStage, "appDelegate_dialer-app");
595+ var dialerAppDelegate = startApplication("dialer-app");
527596
528597 // verify the drop shadow becomes visible
529598 verify(PanelState.dropShadow == true);
@@ -532,7 +601,7 @@ Item {
532601 ApplicationManager.stopApplication("facebook-webapp");
533602
534603 // verify the drop shadow is gone
535− verify(PanelState.dropShadow == false);
604+ tryCompare(PanelState, "dropShadow", false);
536605 }
537606 }
538607}
tests/qmltests/Stages/tst_PhoneStage.qml+255−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,128 @@ 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+ }
479652 }
480653}
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_SurfaceContainer.qml+230−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,40 @@
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 {
42+ id: surfaceContainer
3443 anchors.fill: parent
44+ focus: true
3545 interactive: interactiveCheckbox.checked
3646 }
3747 }
48+
3849 Loader {
3950 id: surfaceContainerLoader
51+ focus: true
4052 anchors {
4153 top: parent.top
4254 topMargin: fullscreenCheckbox.checked ? 0 : units.gu(3)
@@ -56,80 +68,248 @@ Rectangle {
5668 right: parent.right
5769 }
5870
59− Column {
71+ ColumnLayout {
6072 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
6173 spacing: units.gu(1)
62− Row {
63− anchors { left: parent.left; right: parent.right }
74+
75+ RowLayout {
76+ Layout.fillWidth: true
77+
78+ Row {
79+ CheckBox {id: fullscreenCheckbox; checked: true; activeFocusOnPress: false }
80+ Label { text: "fullscreen" }
81+ }
82+ Row {
83+ CheckBox {id: interactiveCheckbox; checked: true; activeFocusOnPress: false }
84+ Label { text: "interactive" }
85+ }
86+ }
87+
88+ RowLayout {
89+ Layout.fillWidth: true
90+
6491 CheckBox {
6592 id: surfaceCheckbox;
6693 checked: false;
94+ activeFocusOnPress: false
6795 onCheckedChanged: {
6896 if (surfaceContainerLoader.status !== Loader.Ready)
6997 return;
7098
7199 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;
100+ var application = ApplicationManager.add("music-app");
101+ application.manualSurfaceCreation = true;
102+ application.setState(ApplicationInfoInterface.Starting);
103+
104+ surfaceContainerLoader.item.surface = SurfaceManager.createSurface("foo",
105+ Mir.NormalType, Mir.MaximizedState, application);
106+
107+ application.setState(ApplicationInfoInterface.Running);
77108 } else {
78− surfaceContainerLoader.item.surface.setLive(false);
109+ if (surfaceContainerLoader.item.surface) {
110+ surfaceContainerLoader.item.surface.setLive(false);
111+ }
112+ ApplicationManager.stopApplication("music-app");
79113 }
80114 }
81115 }
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" }
116+
117+ Label {
118+ text: "surface"
119+ anchors.verticalCenter: parent.verticalCenter
120+ }
88121 }
89− Row {
90− anchors { left: parent.left; right: parent.right }
91− CheckBox {id: interactiveCheckbox; checked: true; }
92− Label { text: "interactive" }
122+
123+ Rectangle {
124+ border {
125+ color: "black"
126+ width: 1
127+ }
128+ anchors {
129+ left: parent.left
130+ right: parent.right
131+ }
132+ Layout.preferredHeight: surfaceChildrenControl.height
133+
134+ RecursingChildSessionControl {
135+ id: surfaceChildrenControl
136+ anchors { left: parent.left; right: parent.right; }
137+
138+ surface: surfaceContainerLoader.item ? surfaceContainerLoader.item.surface : null
139+ }
93140 }
94− MouseTouchEmulationCheckbox {}
95141 }
96142 }
97143
98− SignalSpy {
99− id: surfaceSpy
100− target: SurfaceManager
101− signalName: "surfaceDestroyed"
102− }
103−
104− UnityTestCase {
144+ UT.UnityTestCase {
105145 id: testCase
106− name: "SurfaceContainer"
107− when: windowShown
146+ name: "SessionContainer"
108147
109− property var surfaceContainer:
110− surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
148+ property Item surfaceContainer: surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
111149
112150 function cleanup() {
113151 // reload our test subject to get it in a fresh state once again
114152 surfaceContainerLoader.active = false;
115153 surfaceCheckbox.checked = false;
154+ interactiveCheckbox.checked = true;
116155 surfaceContainerLoader.active = true;
117156
118157 tryCompare(surfaceContainerLoader.item, "surface", null);
119− surfaceSpy.clear();
158+ }
120159
121− interactiveCheckbox.checked = true;
160+ when: windowShown
161+
162+ function test_addChildSession_data() {
163+ return [ { tag: "count=1", count: 1 },
164+ { tag: "count=4", count: 4 } ];
122165 }
123166
124− function test_animateRemoval() {
167+ function test_addChildSession(data) {
125168 surfaceCheckbox.checked = true;
169+ var childSurfaces = testCase.findChild(surfaceContainer, "childSurfacesRepeater");
170+ compare(childSurfaces.count, 0);
126171
127− verify(surfaceContainer.surface !== null);
172+ var i;
173+ for (i = 0; i < data.count; i++) {
174+ surfaceContainer.surface.createPromptSurface();
175+ compare(childSurfaces.count, i+1);
176+ }
177+
178+ for (i = data.count-1; i >= 0; i--) {
179+ {
180+ var childPromptSurface = surfaceContainer.surface.promptSurfaceList.get(i);
181+ childPromptSurface.close();
182+ }
183+ tryCompareFunction(function() { return childSurfaces.count; }, i);
184+ }
185+ }
186+
187+ function test_childSessionDestructionReturnsFocusToSiblingOrParent() {
188+ surfaceCheckbox.checked = true;
189+ var childSurfaces = testCase.findChild(surfaceContainer, "childSurfacesRepeater");
190+ compare(childSurfaces.count, 0);
191+
192+ var i;
193+ // 3 surfaces should cover all edge cases
194+ for (i = 0; i < 3; i++) {
195+ surfaceContainer.surface.createPromptSurface();
196+ compare(childSurfaces.count, i+1);
197+ }
128198
129− surfaceContainer.surface.setLive(false);
199+ for (i = 2; i >= 0; --i) {
200+ var childPromptSurface = surfaceContainer.surface.promptSurfaceList.get(i);
201+ compare(childPromptSurface.activeFocus, true);
130202
131− compare(surfaceContainer.state, "zombie");
132− tryCompare(surfaceContainer, "surface", null);
203+ childPromptSurface.close();
204+ tryCompareFunction(function() { return childSurfaces.count; }, i);
205+
206+ if (i > 0) {
207+ // active focus should have gone to the yongest remaining sibling
208+ var previousSiblingSurface = surfaceContainer.surface.promptSurfaceList.get(i-1);
209+ tryCompare(previousSiblingSurface, "activeFocus", true);
210+ } else {
211+ // active focus should have gone to the parent surface
212+ tryCompare(surfaceContainer.surface, "activeFocus", true);
213+ }
214+ }
215+ }
216+
217+ function test_nestedChildSessions_data() {
218+ return [ { tag: "depth=2", depth: 2 },
219+ { tag: "depth=8", depth: 8 }
220+ ];
221+ }
222+ function test_nestedChildSessions(data) {
223+ surfaceCheckbox.checked = true;
224+
225+ var i;
226+ var container = surfaceContainer;
227+ var surface = container.surface;
228+ var surfaces = [surface];
229+ var parent_childSurfaces = [null];
230+ for (i = 0; i < data.depth; i++) {
231+ surface.createPromptSurface();
232+ var childSurfaces = testCase.findChild(container, "childSurfacesRepeater");
233+ compare(childSurfaces.count, 1);
234+
235+ var childDelegate = childSurfaces.itemAt(0);
236+ container = findChild(childDelegate, "surfaceContainer");
237+ {
238+ var animationsLoader = findChild(container, "animationsLoader");
239+ tryCompare(animationsLoader, "status", Loader.Ready);
240+ waitUntilTransitionsEnd(animationsLoader.item);
241+ }
242+ surface = container.surface;
243+
244+ surfaces.push(surface);
245+ parent_childSurfaces.push(childSurfaces);
246+ }
247+
248+ for (i = surfaces.length-1; i >= 0; i--) {
249+ surfaces[i].close();
250+ if (parent_childSurfaces[i]) {
251+ tryCompareFunction(function() { return parent_childSurfaces[i].count; }, 0);
252+ }
253+ }
254+ }
255+
256+ function test_childrenAdjustForParentSize() {
257+ surfaceCheckbox.checked = true;
258+
259+ surfaceContainer.surface.createPromptSurface();
260+
261+ var delegate = findChild(surfaceContainer, "childDelegate0");
262+ var childContainer = findChild(delegate, "surfaceContainer");
263+
264+ tryCompareFunction(function() { return childContainer.height === surfaceContainer.height; }, true);
265+ tryCompareFunction(function() { return childContainer.width === surfaceContainer.width; }, true);
266+ tryCompareFunction(function() { return childContainer.x === 0; }, true);
267+ tryCompareFunction(function() { return childContainer.y === 0; }, true);
268+
269+ surfaceContainer.anchors.margins = units.gu(2);
270+
271+ tryCompareFunction(function() { return childContainer.height === surfaceContainer.height; }, true);
272+ tryCompareFunction(function() { return childContainer.width === surfaceContainer.width; }, true);
273+ tryCompareFunction(function() { return childContainer.x === 0; }, true);
274+ tryCompareFunction(function() { return childContainer.y === 0; }, true);
275+ }
276+
277+ function isContainerAnimating(container) {
278+ var animationsLoader = findChild(container, "animationsLoader");
279+ if (!animationsLoader || animationsLoader.status != Loader.Ready) {
280+ return false;
281+ }
282+
283+ var animation = animationsLoader.item;
284+
285+ var animating = false;
286+ for (var i = 0; i < animation.transitions.length; ++i) {
287+ if (animation.transitions[i].running) {
288+ return true;
289+ }
290+ }
291+ return false;
292+ }
293+
294+ function test_childrenAnimate() {
295+ surfaceCheckbox.checked = true;
296+
297+ surfaceContainer.surface.createPromptSurface();
298+
299+ var delegate = findChild(surfaceContainer, "childDelegate0");
300+ var childContainer = findChild(delegate, "surfaceContainer");
301+
302+ // wait for animation to begin
303+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
304+ // wait for animation to end
305+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
306+
307+ surfaceContainer.surface.promptSurfaceList.get(0).close();
308+
309+ // wait for animation to begin
310+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
311+ // wait for animation to end
312+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
133313 }
134314
135315 function test_surfaceItemGetsNoTouchesWhenContainerNotInteractive() {
@@ -142,14 +322,14 @@ Rectangle {
142322 surfaceItem.touchPressCount = 0;
143323 surfaceItem.touchReleaseCount = 0;
144324
145− tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
325+ tap(surfaceContainer);
146326
147327 // surface got touches as the surfaceContainer is interactive
148328 compare(surfaceItem.touchPressCount, 1)
149329 compare(surfaceItem.touchReleaseCount, 1);
150330
151331 interactiveCheckbox.checked = false;
152− tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
332+ tap(surfaceContainer);
153333
154334 // surface shouldn't get the touches from the second tap as the surfaceContainer
155335 // was *not* interactive when it happened.
@@ -164,6 +344,8 @@ Rectangle {
164344 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
165345 verify(surfaceItem !== null);
166346
347+ surfaceContainer.focus = false;
348+
167349 compare(surfaceItem.activeFocus, false);
168350 mouseClick(surfaceContainer);
169351 compare(surfaceItem.activeFocus, true);
@@ -176,6 +358,8 @@ Rectangle {
176358 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
177359 verify(surfaceItem !== null);
178360
361+ surfaceContainer.focus = false;
362+
179363 compare(surfaceItem.activeFocus, false);
180364 tap(surfaceContainer);
181365 compare(surfaceItem.activeFocus, true);
tests/qmltests/Stages/tst_TabletStage.qml+250−84
@@ -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
@@ -34,6 +34,11 @@ Rectangle {
3434
3535 property var greeter: { fullyShown: true }
3636
37+ TopLevelSurfaceList {
38+ id: topSurfaceList
39+ applicationsModel: ApplicationManager
40+ }
41+
3742 Loader {
3843 id: tabletStageLoader
3944
@@ -62,6 +67,8 @@ Rectangle {
6267 primary: Qt.LandscapeOrientation
6368 }
6469 focus: true
70+ applicationManager: ApplicationManager
71+ topLevelSurfaceList: topSurfaceList
6572 }
6673 }
6774 }
@@ -76,6 +83,9 @@ Rectangle {
7683 right: parent.right
7784 }
7885
86+ // don't let mouse clicks in the controls area disturb the TabletStage behind it
87+ MouseArea { anchors.fill: parent }
88+
7989 Column {
8090 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8191 spacing: units.gu(1)
@@ -130,18 +140,21 @@ Rectangle {
130140 tabletStageLoader.active = true;
131141 tryCompare(tabletStageLoader, "status", Loader.Ready);
132142
143+ tryCompare(topSurfaceList, "count", 1);
144+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
145+
133146 // this is very strange, but sometimes the test starts without
134147 // TabletStage components having finished loading themselves
135− var appWindow = null;
148+ var appWindow = null
136149 while (!appWindow) {
137− appWindow = findChild(tabletStage, "appWindow_unity8-dash");
150+ appWindow = findAppWindowForSurfaceId(topSurfaceList.idAt(0));
138151 if (!appWindow) {
139− console.log("didn't find appWindow_unity8-dash in " + tabletStage + ". Trying again...");
152+ console.log("didn't find unity8-dash appWindow in " + tabletStage + ". Trying again...");
140153 wait(50);
141154 }
142155 }
143156
144− waitUntilAppSurfaceShowsUp("unity8-dash");
157+ waitUntilAppSurfaceShowsUp(topSurfaceList.idAt(0));
145158 sideStage.hideNow()
146159 }
147160
@@ -165,34 +178,35 @@ Rectangle {
165178 WindowStateStorage.clear();
166179 }
167180
168− function waitUntilAppSurfaceShowsUp(appId) {
169− var appWindow = findChild(tabletStage, "appWindow_" + appId);
181+ function waitUntilAppSurfaceShowsUp(surfaceId) {
182+ var appWindow = findAppWindowForSurfaceId(surfaceId);
170183 verify(appWindow);
171184 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
172185 verify(appWindowStates);
173186 tryCompare(appWindowStates, "state", "surface");
174187 }
175188
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);
189+ function findAppWindowForSurfaceId(surfaceId) {
190+ var spreadDelegate = findChild(tabletStage, "spreadDelegate_" + surfaceId);
191+ if (!spreadDelegate) {
192+ return null;
193+ }
194+ var appWindow = findChild(spreadDelegate, "appWindow");
195+ return appWindow;
196+ }
186197
187− waitUntilAppDelegateStopsMoving(targetAppId);
198+ function switchToSurface(targetSurfaceId) {
199+ performEdgeSwipeToShowAppSpread();
188200
189− var targetAppWindow = findChild(tabletStage, "appWindow_" + targetAppId);
201+ waitUntilAppDelegateStopsMoving(targetSurfaceId);
202+ var targetAppWindow = findAppWindowForSurfaceId(targetSurfaceId);
203+ verify(targetAppWindow);
190204 tap(targetAppWindow, 10, 10);
191205 }
192206
193− function waitUntilAppDelegateStopsMoving(targetAppId)
207+ function waitUntilAppDelegateStopsMoving(targetSurfaceId)
194208 {
195− var targetAppDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + targetAppId);
209+ var targetAppDelegate = findChild(tabletStage, "spreadDelegate_" + targetSurfaceId);
196210 verify(targetAppDelegate);
197211 var lastValue = undefined;
198212 do {
@@ -201,56 +215,86 @@ Rectangle {
201215 } while (lastValue != targetAppDelegate.animatedProgress);
202216 }
203217
218+ function performEdgeSwipeToShowAppSpread() {
219+ touchFlick(tabletStage,
220+ tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
221+ tabletStage.x + 1, tabletStage.height / 2);
222+
223+ var spreadView = findChild(tabletStage, "spreadView");
224+ verify(spreadView);
225+ tryCompare(spreadView, "phase", 2);
226+ tryCompare(spreadView, "flicking", false);
227+ tryCompare(spreadView, "moving", false);
228+ }
229+
230+ function swipeSurfaceUpwards(surfaceId) {
231+ var appWindow = findAppWindowForSurfaceId(surfaceId);
232+ verify(appWindow);
233+
234+ // Swipe from the left side of the surface as it's the one most likely
235+ // to not be covered by other surfaces when they're all being shown in the spread
236+ touchFlick(appWindow,
237+ appWindow.width * 0.1, appWindow.height / 2,
238+ appWindow.width * 0.1, -appWindow.height / 2);
239+ }
240+
204241 function test_tappingSwitchesFocusBetweenStages() {
205242 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
206243
244+ var webbrowserSurfaceId = topSurfaceList.nextId;
207245 webbrowserCheckBox.checked = true;
208− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
246+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
209247 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
210248 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
211− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", true);
249+ var webbrowserWindow = findAppWindowForSurfaceId(webbrowserSurfaceId);
250+ verify(webbrowserWindow);
251+
252+ tryCompare(webbrowserWindow.surface, "activeFocus", true);
212253
254+ var dialerSurfaceId = topSurfaceList.nextId;
213255 dialerCheckBox.checked = true;
214− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
256+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
257+
215258 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);
259+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
260+ verify(dialerDelegate);
261+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
262+
263+ tryCompare(dialerDelegate.surface, "activeFocus", true);
264+ tryCompare(webbrowserWindow.surface, "activeFocus", false);
219265
220266 // Tap on the main stage application and check if the focus
221267 // has been passed to it.
222268
223− var webbrowserWindow = findChild(tabletStage, "appWindow_" + webbrowserApp.appId);
224− verify(webbrowserWindow);
225269 tap(webbrowserWindow);
226270
227− tryCompare(dialerApp.session.lastSurface, "activeFocus", false);
228− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", true);
271+ tryCompare(dialerDelegate.surface, "activeFocus", false);
272+ tryCompare(webbrowserWindow.surface, "activeFocus", true);
229273
230274 // Now tap on the side stage application and check if the focus
231275 // has been passed back to it.
232276
233− var dialerWindow = findChild(tabletStage, "appWindow_" + dialerApp.appId);
234− verify(dialerWindow);
235− tap(dialerWindow);
277+ tap(dialerDelegate);
236278
237− tryCompare(dialerApp.session.lastSurface, "activeFocus", true);
238− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", false);
279+ tryCompare(dialerDelegate.surface, "activeFocus", true);
280+ tryCompare(webbrowserWindow.surface, "activeFocus", false);
239281 }
240282
241283 function test_closeAppInSideStage() {
242284 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
243285
286+ var dialerSurfaceId = topSurfaceList.nextId;
244287 dialerCheckBox.checked = true;
245− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
288+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
246289
247290 performEdgeSwipeToShowAppSpread();
248291
249− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + dialerCheckBox.appId);
292+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
250293 verify(appDelegate);
294+ compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
251295 tryCompare(appDelegate, "swipeToCloseEnabled", true);
252296
253− swipeAppUpwards(dialerCheckBox.appId);
297+ swipeSurfaceUpwards(dialerSurfaceId);
254298
255299 // Check that dialer-app has been closed
256300
@@ -263,54 +307,37 @@ Rectangle {
263307 }, null);
264308 }
265309
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−
288310 function test_suspendsAndResumesAppsInMainStage() {
311+ var webbrowserSurfaceId = topSurfaceList.nextId;
289312 webbrowserCheckBox.checked = true;
313+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
290314 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
291− compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
315+ var webbrowserDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
316+ compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
292317
293318 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
294319
320+ var gallerySurfaceId = topSurfaceList.nextId;
295321 galleryCheckBox.checked = true;
322+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
296323 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
297− compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
324+ var galleryDelegate = findChild(tabletStage, "spreadDelegate_" + gallerySurfaceId);
325+ compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
298326
299327 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
300328 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
301329
302− switchToApp(webbrowserApp.appId);
330+ switchToSurface(webbrowserSurfaceId);
303331
304332 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
305333 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
306334
307− switchToApp(galleryApp.appId);
335+ switchToSurface(gallerySurfaceId);
308336
309337 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
310338 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
311339 }
312340
313−
314341 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
315342 WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
316343 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
@@ -321,30 +348,37 @@ Rectangle {
321348 // launch two main stage apps
322349 // gallery will be on foreground and webbrowser on background
323350
351+ var webbrowserSurfaceId = topSurfaceList.nextId;
324352 webbrowserCheckBox.checked = true;
325− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
353+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
326354 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
327355 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
328356
357+ var gallerySurfaceId = topSurfaceList.nextId;
329358 galleryCheckBox.checked = true;
330− waitUntilAppSurfaceShowsUp(galleryCheckBox.appId)
359+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
331360 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
332− compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
361+ var galleryDelegate = findChild(tabletStage, "spreadDelegate_" + gallerySurfaceId);
362+ compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
333363
334364 compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
335365
336366 // then launch two side stage apps
337367 // facebook will be on foreground and dialer on background
338368
369+ var dialerSurfaceId = topSurfaceList.nextId;
339370 dialerCheckBox.checked = true;
340− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
371+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
341372 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
342− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
373+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
374+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
343375
376+ var facebookSurfaceId = topSurfaceList.nextId;
344377 facebookCheckBox.checked = true;
345− waitUntilAppSurfaceShowsUp(facebookCheckBox.appId)
378+ waitUntilAppSurfaceShowsUp(facebookSurfaceId);
346379 var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
347− compare(facebookApp.stage, ApplicationInfoInterface.SideStage);
380+ var facebookDelegate = findChild(tabletStage, "spreadDelegate_" + facebookSurfaceId);
381+ compare(facebookDelegate.stage, ApplicationInfoInterface.SideStage);
348382
349383 compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
350384
@@ -356,14 +390,14 @@ Rectangle {
356390 tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
357391 tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
358392
359− switchToApp(dialerCheckBox.appId);
393+ switchToSurface(dialerSurfaceId);
360394
361395 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
362396 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
363397 tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
364398 tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
365399
366− switchToApp(webbrowserCheckBox.appId);
400+ switchToSurface(webbrowserSurfaceId);
367401
368402 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
369403 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
@@ -374,15 +408,19 @@ Rectangle {
374408 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
375409 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
376410
411+ var webbrowserSurfaceId = topSurfaceList.nextId;
377412 webbrowserCheckBox.checked = true;
378− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
413+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
379414 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
380− compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
415+ var webbrowserDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
416+ compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
381417
418+ var dialerSurfaceId = topSurfaceList.nextId;
382419 dialerCheckBox.checked = true;
383− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
420+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
384421 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
385− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
422+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
423+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
386424
387425
388426 compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
@@ -467,8 +505,9 @@ Rectangle {
467505 tryCompare(stagesPriv, "mainStageAppId", "unity8-dash");
468506 tryCompare(stagesPriv, "sideStageAppId", "");
469507
508+ var webbrowserSurfaceId = topSurfaceList.nextId;
470509 webbrowserCheckBox.checked = true;
471− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
510+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
472511
473512 tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
474513 tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
@@ -476,10 +515,11 @@ Rectangle {
476515
477516 function test_loadSideStageByDragginFromMainStage() {
478517 sideStage.showNow();
518+ var webbrowserSurfaceId = topSurfaceList.nextId;
479519 webbrowserCheckBox.checked = true;
480− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
520+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
481521
482− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
522+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
483523 verify(appDelegate);
484524 compare(appDelegate.stage, ApplicationInfoInterface.MainStage);
485525
@@ -504,10 +544,11 @@ Rectangle {
504544 function test_unloadSideStageByDragginFromStageStage() {
505545 sideStage.showNow();
506546 WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
547+ var webbrowserSurfaceId = topSurfaceList.nextId;
507548 webbrowserCheckBox.checked = true;
508− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
549+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
509550
510− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
551+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
511552 verify(appDelegate);
512553 compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
513554
@@ -527,5 +568,130 @@ Rectangle {
527568
528569 tryCompare(appDelegate, "stage", ApplicationInfoInterface.MainStage);
529570 }
571+
572+ function test_closeSurfaceOfMultiSurfaceApp() {
573+ var surface1Id = topSurfaceList.nextId;
574+ webbrowserCheckBox.checked = true;
575+ waitUntilAppSurfaceShowsUp(surface1Id);
576+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
577+
578+ var surface2Id = topSurfaceList.nextId;
579+ verify(surface1Id !== surface2Id); // sanity checking
580+ webbrowserCheckBox.createSurface();
581+ waitUntilAppSurfaceShowsUp(surface2Id);
582+
583+ performEdgeSwipeToShowAppSpread();
584+
585+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + surface1Id);
586+ verify(appDelegate);
587+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
588+
589+ compare(webbrowserApp.surfaceList.count, 2);
590+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
591+
592+ swipeSurfaceUpwards(surface1Id);
593+
594+ // Surface must eventually be gone
595+ tryCompareFunction(function() { return topSurfaceList.indexForId(surface1Id); }, -1);
596+ tryCompare(webbrowserApp.surfaceList, "count", 1);
597+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
598+ }
599+
600+ /*
601+ 1- Suspended app gets killed behind the scenes, causing its surface to go zombie.
602+ 2- Surface gets screenshotted and removed. Its slot in the topSurfaceList remains,
603+ though (so ApplicationWindow can display the screenshot in its place).
604+ 3- User taps on the screenshot of the long-gone surface.
605+
606+ Expected outcome:
607+ Application gets relaunched. Its new surface will seamlessly replace the screenshot.
608+ */
609+ function test_selectSuspendedAppWithoutSurface() {
610+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
611+ var dashSurfaceId = topSurfaceList.idAt(0);
612+ var dashSurface = topSurfaceList.surfaceAt(0);
613+
614+ var webbrowserSurfaceId = topSurfaceList.nextId;
615+ webbrowserCheckBox.checked = true;
616+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
617+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
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(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
632+
633+ switchToSurface(webbrowserSurfaceId);
634+
635+ // webbrowser should have been brought to front
636+ tryCompareFunction(function(){return topSurfaceList.idAt(0);}, webbrowserSurfaceId);
637+
638+ // and it should eventually get a new surface and get resumed
639+ tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) !== null;}, true);
640+ compare(topSurfaceList.count, 2); // still two top-level items
641+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
642+ compare(webbrowserApp.surfaceList.count, 1);
643+ }
644+
645+ /*
646+ 1 - user suspends an application (ie, focus a surface from a different application)
647+ 2 - That suspended application gets killed, causing its surface to go zombie
648+ 3 - user goes to spread and closes that zombie surface. Actually, by that time
649+ Tablet shell will be displaying a screenshot of it instead, so there's no
650+ MirSurface whatsoever backing it up.
651+ */
652+ function test_closeZombieSurface()
653+ {
654+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
655+ var dashSurfaceId = topSurfaceList.idAt(0);
656+ var dashSurface = topSurfaceList.surfaceAt(0);
657+
658+ var webbrowserSurfaceId = topSurfaceList.nextId;
659+ webbrowserCheckBox.checked = true;
660+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
661+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
662+
663+ switchToSurface(dashSurfaceId);
664+
665+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
666+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
667+
668+ compare(webbrowserApp.surfaceList.count, 1);
669+
670+ // simulate the suspended app being killed by the out-of-memory daemon
671+ webbrowserApp.surfaceList.get(0).setLive(false);
672+
673+ // wait until the surface is gone
674+ tryCompare(webbrowserApp.surfaceList, "count", 0);
675+ compare(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
676+
677+ performEdgeSwipeToShowAppSpread();
678+
679+ {
680+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
681+ verify(appDelegate);
682+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
683+ }
684+
685+ swipeSurfaceUpwards(webbrowserSurfaceId);
686+
687+ // webbrowser entry is nowhere to be seen
688+ tryCompareFunction(function(){return topSurfaceList.indexForId(webbrowserSurfaceId);}, -1);
689+
690+ // nor is its app
691+ compare(ApplicationManager.findApplication(webbrowserCheckBox.appId), null);
692+
693+ // only unity8-dash surface is left
694+ compare(topSurfaceList.count, 1);
695+ }
530696 }
531697}
tests/qmltests/Tutorial/tst_Tutorial.qml−8
@@ -280,14 +280,6 @@ Rectangle {
280280 prepareShell();
281281 }
282282
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−
291283 function swipeAwayGreeter() {
292284 var coverPage = findChild(shell, "coverPage");
293285 tryCompare(coverPage, "showProgress", 1);
tests/qmltests/tst_OrientedShell.qml+76−55
@@ -24,6 +24,7 @@ import Unity.Test 0.1
2424import IntegratedLightDM 0.1 as LightDM
2525import Powerd 0.1
2626import Unity.InputInfo 0.1
27+import Utils 0.1
2728
2829import "../../qml"
2930
@@ -434,6 +435,7 @@ Rectangle {
434435
435436 property Item orientedShell: orientedShellLoader.status === Loader.Ready ? orientedShellLoader.item : null
436437 property Item shell
438+ property QtObject topLevelSurfaceList
437439
438440 SignalSpy { id: signalSpy }
439441 SignalSpy { id: signalSpy2 }
@@ -485,6 +487,7 @@ Rectangle {
485487 function cleanup() {
486488 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
487489 shell = null;
490+ topLevelSurfaceList = null;
488491
489492 tearDown();
490493 }
@@ -533,17 +536,20 @@ Rectangle {
533536 function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape(data) {
534537 loadShell(data.deviceName);
535538
539+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
540+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
541+ var dashAppWindow = findAppWindowForSurfaceId(dashSurfaceId);
542+
536543 // unity8-dash supports only primary orientation and should be already running
537− var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
538− verify(dashAppWindow);
544+
539545 compare(ApplicationManager.focusedApplicationId, "unity8-dash");
540− var dashApp = dashAppWindow.application
546+ var dashApp = dashAppWindow.application;
541547 verify(dashApp);
542548 compare(dashApp.rotatesWindowContents, false);
543549 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
544550 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
545551
546− tryCompareFunction(function(){return dashApp.session.lastSurface != null;}, true);
552+ tryCompareFunction(function(){return dashApp.surfaceList.count > 0;}, true);
547553 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
548554
549555 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
@@ -610,6 +616,7 @@ Rectangle {
610616 }
611617 function test_appRotatesWindowContents(data) {
612618 loadShell(data.deviceName);
619+ var cameraSurfaceId = topLevelSurfaceList.nextId;
613620 var cameraApp = ApplicationManager.startApplication("camera-app");
614621 verify(cameraApp);
615622
@@ -619,9 +626,9 @@ Rectangle {
619626 compare(cameraApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
620627 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
621628
622− waitUntilAppSurfaceShowsUp("camera-app")
629+ waitUntilAppWindowIsFullyLoaded(cameraSurfaceId);
623630
624− var cameraSurface = cameraApp.session.lastSurface;
631+ var cameraSurface = cameraApp.surfaceList.get(0);
625632 verify(cameraSurface);
626633
627634 var focusChangedSpy = signalSpy;
@@ -687,6 +694,7 @@ Rectangle {
687694 }
688695 function test_switchingToAppWithDifferentRotation(data) {
689696 loadShell(data.deviceName);
697+ var gmailSurfaceId = topLevelSurfaceList.nextId;
690698 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
691699 verify(gmailApp);
692700
@@ -696,8 +704,9 @@ Rectangle {
696704 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
697705 compare(gmailApp.stage, ApplicationInfoInterface.MainStage);
698706
699− waitUntilAppSurfaceShowsUp("gmail-webapp")
707+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
700708
709+ var musicSurfaceId = topLevelSurfaceList.nextId;
701710 var musicApp = ApplicationManager.startApplication("music-app");
702711 verify(musicApp);
703712
@@ -707,7 +716,7 @@ Rectangle {
707716 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
708717 compare(musicApp.stage, ApplicationInfoInterface.MainStage);
709718
710− waitUntilAppSurfaceShowsUp("music-app")
719+ waitUntilAppWindowIsFullyLoaded(musicSurfaceId);
711720 tryCompare(shell, "orientationChangesEnabled", true);
712721
713722 rotateTo(90);
@@ -742,6 +751,7 @@ Rectangle {
742751 */
743752 function test_rotateToUnsupportedDeviceOrientation(data) {
744753 loadShell("mako");
754+ var twitterSurfaceId = topLevelSurfaceList.nextId;
745755 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
746756 verify(twitterApp);
747757
@@ -750,7 +760,7 @@ Rectangle {
750760 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
751761 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
752762
753− waitUntilAppSurfaceShowsUp("twitter-webapp")
763+ waitUntilAppWindowIsFullyLoaded(twitterSurfaceId);
754764
755765 rotateTo(data.rotationAngle);
756766 tryCompare(shell, "transformRotationAngle", data.rotationAngle);
@@ -767,13 +777,14 @@ Rectangle {
767777
768778 function test_launchLandscapeOnlyAppFromPortrait() {
769779 loadShell("mako");
780+ var weatherSurfaceId = topLevelSurfaceList.nextId;
770781 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
771782 verify(weatherApp);
772783
773784 // ensure the mock app is as we expect
774785 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
775786
776− waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
787+ waitUntilAppWindowIsFullyLoaded(weatherSurfaceId);
777788
778789 var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
779790 waitUntilTransitionsEnd(rotationStates);
@@ -805,6 +816,7 @@ Rectangle {
805816 */
806817 function test_greeterStaysAwayAfterRotation() {
807818 loadShell("mako");
819+ var twitterSurfaceId = topLevelSurfaceList.nextId;
808820 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
809821 verify(twitterApp);
810822
@@ -813,8 +825,8 @@ Rectangle {
813825 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
814826 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
815827
816− waitUntilAppSurfaceShowsUp("twitter-webapp");
817− waitUntilAppWindowCanRotate("twitter-webapp");
828+ waitUntilAppWindowIsFullyLoaded(twitterSurfaceId);
829+ waitUntilAppWindowCanRotate(twitterSurfaceId);
818830
819831 // go back to unity8-dash
820832 performEdgeSwipeToSwitchToPreviousApp();
@@ -830,7 +842,7 @@ Rectangle {
830842 var greeter = findChild(shell, "greeter");
831843 tryCompare(greeter, "animating", false);
832844
833− var twitterDelegate = findChild(shell, "appDelegate1");
845+ var twitterDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
834846 compare(twitterDelegate.application.appId, "twitter-webapp");
835847 twitterDelegate.clicked();
836848
@@ -1018,13 +1030,15 @@ Rectangle {
10181030 function test_launchedAppHasActiveFocus(data) {
10191031 loadShell(data.deviceName);
10201032
1033+ var gmailSurfaceId = topLevelSurfaceList.nextId;
10211034 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
10221035 verify(gmailApp);
1023− waitUntilAppSurfaceShowsUp("gmail-webapp")
1036+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
10241037
1025− verify(gmailApp.session.lastSurface);
1038+ var gmailSurface = gmailApp.surfaceList.get(0);
1039+ verify(gmailSurface);
10261040
1027− tryCompare(gmailApp.session.lastSurface, "activeFocus", true);
1041+ tryCompare(gmailSurface, "activeFocus", true);
10281042 }
10291043
10301044 function test_launchLandscapeOnlyAppOverPortraitOnlyDashThenSwitchToDash() {
@@ -1033,13 +1047,14 @@ Rectangle {
10331047 // starts as portrait, as unity8-dash is portrait only
10341048 tryCompare(shell, "transformRotationAngle", 0);
10351049
1050+ var weatherSurfaceId = topLevelSurfaceList.nextId;
10361051 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
10371052 verify(weatherApp);
10381053
10391054 // ensure the mock app is as we expect
10401055 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
10411056
1042− waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
1057+ waitUntilAppWindowIsFullyLoaded(weatherSurfaceId);
10431058
10441059 // should have rotated to landscape
10451060 tryCompareFunction(function () { return shell.transformRotationAngle == 270
@@ -1233,10 +1248,14 @@ Rectangle {
12331248 function test_lockPhoneAfterClosingAppInSpreadThenUnlockAndRotate() {
12341249 loadShell("mako");
12351250
1251+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
1252+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
1253+
1254+ var gmailSurfaceId = topLevelSurfaceList.nextId;
12361255 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
12371256 verify(gmailApp);
12381257
1239− waitUntilAppSurfaceShowsUp("gmail-webapp");
1258+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
12401259
12411260 performEdgeSwipeToShowAppSpread();
12421261
@@ -1252,7 +1271,7 @@ Rectangle {
12521271
12531272 swipeAwayGreeter();
12541273
1255− verify(isAppSurfaceFocused("unity8-dash"))
1274+ verify(isAppSurfaceFocused(dashSurfaceId))
12561275
12571276 signalSpy.clear();
12581277 signalSpy.target = shell;
@@ -1314,6 +1333,7 @@ Rectangle {
13141333 ////
13151334 // Launch a portrait-only application
13161335
1336+ var dialerSurfaceId = topLevelSurfaceList.nextId;
13171337 var dialerApp = ApplicationManager.startApplication("dialer-app");
13181338 verify(dialerApp);
13191339
@@ -1321,9 +1341,9 @@ Rectangle {
13211341 compare(dialerApp.rotatesWindowContents, false);
13221342 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
13231343
1324− waitUntilAppSurfaceShowsUp("dialer-app");
1325− waitUntilAppWindowCanRotate("dialer-app");
1326− verify(isAppSurfaceFocused("dialer-app"));
1344+ waitUntilAppWindowIsFullyLoaded(dialerSurfaceId);
1345+ waitUntilAppWindowCanRotate(dialerSurfaceId);
1346+ verify(isAppSurfaceFocused(dialerSurfaceId));
13271347
13281348 ////
13291349 // check outcome (shell should stay in landscape)
@@ -1376,17 +1396,29 @@ Rectangle {
13761396 tryCompare(appWindowStates, "state", "surface");
13771397 }
13781398
1379− function waitUntilAppSurfaceShowsUp(appId) {
1380− var appWindow = findChild(shell, "appWindow_" + appId);
1381− verify(appWindow);
1382− var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
1383− verify(appWindowStates);
1384− tryCompare(appWindowStates, "state", "surface");
1399+ function findAppWindowForSurfaceId(surfaceId) {
1400+ // for PhoneStage and TabletStage
1401+ var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
1402+ if (!delegate) {
1403+ // for DesktopStage
1404+ delegate = findChild(shell, "appDelegate_" + surfaceId);
1405+ }
1406+ verify(delegate);
1407+ var appWindow = findChild(delegate, "appWindow");
1408+ return appWindow;
13851409 }
13861410
1387− function waitUntilAppWindowCanRotate(appId) {
1388− var appWindow = findChild(shell, "appWindow_" + appId);
1389− verify(appWindow);
1411+ // Wait until the ApplicationWindow for the given Application object is fully loaded
1412+ // (ie, the real surface has replaced the splash screen)
1413+ function waitUntilAppWindowIsFullyLoaded(surfaceId) {
1414+ var appWindow = findAppWindowForSurfaceId(surfaceId);
1415+ var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
1416+ tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
1417+ waitUntilTransitionsEnd(appWindowStateGroup);
1418+ }
1419+
1420+ function waitUntilAppWindowCanRotate(surfaceId) {
1421+ var appWindow = findAppWindowForSurfaceId(surfaceId);
13901422 tryCompare(appWindow, "orientationChangesEnabled", true);
13911423 }
13921424
@@ -1498,14 +1530,6 @@ Rectangle {
14981530 tryCompare(greeter, "fullyShown", true);
14991531 }
15001532
1501− function killApps() {
1502− while (ApplicationManager.count > 1) {
1503− var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
1504− ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
1505− }
1506− compare(ApplicationManager.count, 1)
1507− }
1508−
15091533 function loadShell(deviceName) {
15101534 applicationArguments.deviceName = deviceName;
15111535
@@ -1517,6 +1541,12 @@ Rectangle {
15171541
15181542 shell = findChild(orientedShell, "shell");
15191543
1544+ topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
1545+ verify(topLevelSurfaceList);
1546+
1547+ var stageLoader = findChild(shell, "applicationsDisplayLoader");
1548+ verify(stageLoader);
1549+
15201550 tryCompare(shell, "enabled", true); // enabled by greeter when ready
15211551
15221552 waitUntilShellIsInOrientation(root.physicalOrientation0);
@@ -1525,7 +1555,7 @@ Rectangle {
15251555
15261556 swipeAwayGreeter();
15271557
1528− var spreadRepeater = findChild(shell, "spreadRepeater");
1558+ var spreadRepeater = findChild(stageLoader, "spreadRepeater");
15291559 if (spreadRepeater) {
15301560 spreadRepeaterConnections.target = spreadRepeater;
15311561 }
@@ -1546,7 +1576,7 @@ Rectangle {
15461576
15471577 // expectedAngle is in orientedShell's coordinate system
15481578 function checkAppSurfaceOrientation(item, app, expectedAngle) {
1549− var surface = app.session.lastSurface;
1579+ var surface = app.surfaceList.get(0);
15501580 if (!surface) {
15511581 console.warn("no surface");
15521582 return false;
@@ -1601,7 +1631,7 @@ Rectangle {
16011631 var spreadView = findChild(shell, "spreadView");
16021632 verify(spreadView);
16031633
1604− var delegateToClose = findChild(spreadView, "appDelegate0");
1634+ var delegateToClose = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
16051635 verify(delegateToClose);
16061636
16071637 var appIdToClose = ApplicationManager.get(0).appId;;
@@ -1627,20 +1657,11 @@ Rectangle {
16271657 compare(ApplicationManager.findApplication(appIdToClose), null);
16281658 }
16291659
1630− function isAppSurfaceFocused(appId) {
1631− var appWindow = findChild(shell, "appWindow_" + appId);
1632− verify(appWindow);
1633−
1634− var app = ApplicationManager.findApplication(appId);
1635− verify(app);
1636−
1637− var surface = app.session.lastSurface;
1660+ function isAppSurfaceFocused(surfaceId) {
1661+ var index = topLevelSurfaceList.indexForId(surfaceId);
1662+ var surface = topLevelSurfaceList.surfaceAt(index);
16381663 verify(surface);
1639−
1640− var surfaceItem = findSurfaceItem(appWindow, surface);
1641− verify(surfaceItem);
1642−
1643− return surfaceItem.activeFocus;
1664+ return surface.activeFocus;
16441665 }
16451666 }
16461667}
tests/qmltests/tst_Shell.qml+182−116
@@ -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
@@ -185,6 +185,17 @@ Rectangle {
185185 }
186186 }
187187 }
188+ Button {
189+ text: "Show Launcher"
190+ activeFocusOnPress: false
191+ onClicked: {
192+ if (shellLoader.status !== Loader.Ready)
193+ return;
194+
195+ var launcher = testCase.findChild(shellLoader.item, "launcher");
196+ launcher.state = "visible";
197+ }
198+ }
188199 ListItem.ItemSelector {
189200 anchors { left: parent.left; right: parent.right }
190201 activeFocusOnPress: false
@@ -381,12 +392,6 @@ Rectangle {
381392 signalName: "actionInvoked"
382393 }
383394
384− SignalSpy {
385− id: appRemovedSpy
386− target: ApplicationManager
387− signalName: "applicationRemoved"
388− }
389−
390395 Telephony.CallEntry {
391396 id: phoneCall
392397 phoneNumber: "+447812221111"
@@ -412,6 +417,7 @@ Rectangle {
412417 when: windowShown
413418
414419 property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
420+ property QtObject topLevelSurfaceList: null
415421
416422 function init() {
417423 if (shellLoader.active) {
@@ -449,6 +455,9 @@ Rectangle {
449455 panel.dismissTimer = fakeDismissTimer;
450456
451457 waitForGreeterToStabilize();
458+
459+ topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
460+ verify(topLevelSurfaceList);
452461 }
453462
454463 function loadDesktopShellWithApps() {
@@ -463,7 +472,9 @@ Rectangle {
463472 var app5 = ApplicationManager.startApplication("camera-app")
464473 var app6 = ApplicationManager.startApplication("gallery-app")
465474 var app7 = ApplicationManager.startApplication("calendar-app")
466− waitUntilAppWindowIsFullyLoaded(app7);
475+ for (var i = 0; i < topLevelSurfaceList.count; ++i) {
476+ waitUntilAppWindowIsFullyLoaded(topLevelSurfaceList.idAt(i));
477+ }
467478 }
468479
469480 function waitForGreeterToStabilize() {
@@ -503,7 +514,7 @@ Rectangle {
503514 Wizard.System.wizardEnabled = false;
504515
505516 // kill all (fake) running apps
506− killApps(ApplicationManager);
517+ killApps();
507518
508519 unlockAllModemsSpy.clear()
509520 LightDM.Greeter.authenticate(""); // reset greeter
@@ -511,14 +522,10 @@ Rectangle {
511522 sessionSpy.clear();
512523
513524 GSettingsController.setLifecycleExemptAppids([]);
514− }
515525
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)
526+ // there should be only unity8-dash window over there
527+ tryCompare(ApplicationManager, "count", 1);
528+ compare(ApplicationManager.get(0).appId, "unity8-dash");
522529 }
523530
524531 function test_snapDecisionDismissalReturnsFocus() {
@@ -527,10 +534,13 @@ Rectangle {
527534 var notifications = findChild(shell, "notificationList");
528535 var app = ApplicationManager.startApplication("camera-app");
529536 var stage = findChild(shell, "stage")
530− // Open an application and focus
537+ // Open an application
531538 waitUntilApplicationWindowIsFullyVisible(app);
532− ApplicationManager.focusApplication(app);
533− tryCompare(app.session.lastSurface, "activeFocus", true);
539+
540+ var appSurface = app.surfaceList.get(0);
541+ verify(appSurface);
542+
543+ tryCompare(appSurface, "activeFocus", true);
534544
535545 notifications.model = mockNotificationsModel;
536546
@@ -552,7 +562,7 @@ Rectangle {
552562 waitForRendering(notification);
553563
554564 // Make sure activeFocus went away from the app window
555− tryCompare(app.session.lastSurface, "activeFocus", false);
565+ tryCompare(appSurface, "activeFocus", false);
556566 tryCompare(stage, "interactive", false);
557567
558568 // Clicking the button should dismiss the notification and return focus
@@ -560,7 +570,7 @@ Rectangle {
560570 mouseClick(buttonAccept);
561571
562572 // Make sure we're back to normal
563− tryCompare(app.session.lastSurface, "activeFocus", true);
573+ tryCompare(appSurface, "activeFocus", true);
564574 compare(stage.interactive, true, "Stages not interactive again after modal notification has closed");
565575 }
566576
@@ -644,9 +654,12 @@ Rectangle {
644654 var launcher = findChild(shell, "launcherPanel");
645655 tryCompare(launcher, "x", data.launcherHides ? -launcher.width : 0)
646656
657+ //FIXME: This check fails. Don't understand the rationale behind it.
658+ /*
647659 // Make sure the helper for sliding out the launcher wasn't touched. We want to fade it out here.
648660 var animateTimer = findInvisibleChild(shell, "animateTimer");
649661 compare(animateTimer.nextState, "visible");
662+ */
650663 }
651664
652665 function test_tabletLeftEdgeDrag_data() {
@@ -873,10 +886,22 @@ Rectangle {
873886 tryCompare(item, "visible", false);
874887 }
875888
889+ function findAppWindowForSurfaceId(surfaceId) {
890+ // for PhoneStage and TabletStage
891+ var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
892+ if (!delegate) {
893+ // for DesktopStage
894+ delegate = findChild(shell, "appDelegate_" + surfaceId);
895+ }
896+ verify(delegate);
897+ var appWindow = findChild(delegate, "appWindow");
898+ return appWindow;
899+ }
900+
876901 // Wait until the ApplicationWindow for the given Application object is fully loaded
877902 // (ie, the real surface has replaced the splash screen)
878− function waitUntilAppWindowIsFullyLoaded(app) {
879− var appWindow = findChild(shell, "appWindow_" + app.appId);
903+ function waitUntilAppWindowIsFullyLoaded(surfaceId) {
904+ var appWindow = findAppWindowForSurfaceId(surfaceId);
880905 var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
881906 tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
882907 waitUntilTransitionsEnd(appWindowStateGroup);
@@ -885,10 +910,13 @@ Rectangle {
885910 function test_surfaceLosesActiveFocusWhilePanelIsOpen() {
886911 loadShell("phone");
887912 swipeAwayGreeter();
913+ var appSurfaceId = topLevelSurfaceList.nextId;
888914 var app = ApplicationManager.startApplication("dialer-app");
889− waitUntilAppWindowIsFullyLoaded(app);
915+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
916+ var appSurface = app.surfaceList.get(0);
917+ verify(appSurface);
890918
891− tryCompare(app.session.lastSurface, "activeFocus", true);
919+ tryCompare(appSurface, "activeFocus", true);
892920
893921 // Drag the indicators panel half-open
894922 var touchX = shell.width / 2;
@@ -899,7 +927,7 @@ Rectangle {
899927 true /* beginTouch */, false /* endTouch */);
900928 verify(indicators.partiallyOpened);
901929
902− tryCompare(app.session.lastSurface, "activeFocus", false);
930+ tryCompare(appSurface, "activeFocus", false);
903931
904932 // And finish getting it open
905933 touchFlick(indicators,
@@ -908,11 +936,11 @@ Rectangle {
908936 false /* beginTouch */, true /* endTouch */);
909937 tryCompare(indicators, "fullyOpened", true);
910938
911− tryCompare(app.session.lastSurface, "activeFocus", false);
939+ tryCompare(appSurface, "activeFocus", false);
912940
913941 dragToCloseIndicatorsPanel();
914942
915− tryCompare(app.session.lastSurface, "activeFocus", true);
943+ tryCompare(appSurface, "activeFocus", true);
916944 }
917945
918946 function test_launchedAppHasActiveFocus_data() {
@@ -928,54 +956,44 @@ Rectangle {
928956 shell.usageScenario = data.usageScenario;
929957 swipeAwayGreeter();
930958
959+ var webAppSurfaceId = topLevelSurfaceList.nextId;
931960 var webApp = ApplicationManager.startApplication("webbrowser-app");
932961 verify(webApp);
933− waitUntilAppSurfaceShowsUp("webbrowser-app")
962+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
934963
935− verify(webApp.session.lastSurface);
964+ var webAppSurface = webApp.surfaceList.get(topLevelSurfaceList.indexForId(webAppSurfaceId));
965+ verify(webAppSurface);
936966
937− tryCompare(webApp.session.lastSurface, "activeFocus", true);
967+ tryCompare(webAppSurface, "activeFocus", true);
938968 }
939969
940970 function test_launchedAppKeepsActiveFocusOnUsageModeChange() {
941971 loadShell("tablet");
942972 swipeAwayGreeter();
943973
974+ var webAppSurfaceId = topLevelSurfaceList.nextId;
944975 var webApp = ApplicationManager.startApplication("webbrowser-app");
945976 verify(webApp);
946− waitUntilAppSurfaceShowsUp("webbrowser-app")
977+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
947978
948− verify(webApp.session.lastSurface);
979+ var webAppSurface = webApp.surfaceList.get(topLevelSurfaceList.indexForId(webAppSurfaceId));
980+ verify(webAppSurface);
949981
950− tryCompare(webApp.session.lastSurface, "activeFocus", true);
982+ tryCompare(webAppSurface, "activeFocus", true);
951983
952984 shell.usageScenario = "desktop";
953985
954986 // check that the desktop stage and window have been loaded
955− {
956− var desktopWindow = findChild(shell, "appWindow_webbrowser-app");
957− verify(desktopWindow);
958− }
987+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
959988
960− tryCompare(webApp.session.lastSurface, "activeFocus", true);
989+ tryCompare(webAppSurface, "activeFocus", true);
961990
962991 shell.usageScenario = "tablet";
963992
964993 // 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− }
969−
970− tryCompare(webApp.session.lastSurface, "activeFocus", true);
971− }
994+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
972995
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");
996+ tryCompare(webAppSurface, "activeFocus", true);
979997 }
980998
981999 function dragToCloseIndicatorsPanel() {
@@ -1026,7 +1044,7 @@ Rectangle {
10261044 }
10271045
10281046 function waitUntilApplicationWindowIsFullyVisible() {
1029− var appDelegate = findChild(shell, "appDelegate0")
1047+ var appDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
10301048 var surfaceContainer = findChild(appDelegate, "surfaceContainer");
10311049 tryCompareFunction(function() { return surfaceContainer.surface !== null; }, true);
10321050 }
@@ -1088,8 +1106,7 @@ Rectangle {
10881106 var greeter = findChild(shell, "greeter");
10891107
10901108 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);
1109+ waitUntilApplicationWindowIsFullyVisible(app);
10931110
10941111 // Minimize the application we just launched
10951112 swipeFromLeftEdge(shell.width * 0.75);
@@ -1150,13 +1167,15 @@ Rectangle {
11501167 swipeAwayGreeter();
11511168 var panel = findChild(shell, "panel");
11521169 compare(panel.fullscreenMode, false);
1153− ApplicationManager.startApplication("camera-app");
1170+ var cameraApp = ApplicationManager.startApplication("camera-app");
1171+ waitUntilApplicationWindowIsFullyVisible(cameraApp);
11541172 tryCompare(panel, "fullscreenMode", true);
1155− ApplicationManager.startApplication("dialer-app");
1173+ var dialerApp = ApplicationManager.startApplication("dialer-app");
1174+ waitUntilApplicationWindowIsFullyVisible(dialerApp);
11561175 tryCompare(panel, "fullscreenMode", false);
1157− ApplicationManager.requestFocusApplication("camera-app");
1176+ ApplicationManager.requestFocusApplication(cameraApp.appId);
11581177 tryCompare(panel, "fullscreenMode", true);
1159− ApplicationManager.requestFocusApplication("dialer-app");
1178+ ApplicationManager.requestFocusApplication(dialerApp.appId);
11601179 tryCompare(panel, "fullscreenMode", false);
11611180 }
11621181
@@ -1227,24 +1246,32 @@ Rectangle {
12271246 var tutorial = findChild(shell, "tutorial");
12281247 tryCompare(wizard, "active", true);
12291248 tryCompare(tutorial, "running", true);
1230− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1249+ tryCompareFunction(function() { return topLevelSurfaceList.applicationAt(0).appId; }, "unity8-dash");
12311250
1232− // Make sure we stay running when nothing focused (can happen for
1251+ // Make sure we stay running when there's no top level window (can happen for
12331252 // a moment when we restart the dash after switching language)
1234− ApplicationManager.stopApplication("unity8-dash");
1235− tryCompare(ApplicationManager, "focusedApplicationId", "");
1253+ var dashApplication = ApplicationManager.findApplication("unity8-dash");
1254+ ApplicationManager.stopApplication(dashApplication.appId);
1255+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
1256+ // They won't be gone until those surface items are destroyed.
1257+ tryCompareFunction(function() { return dashApplication.surfaceList.count }, 0);
1258+
1259+ tryCompare(topLevelSurfaceList, "count", 0);
12361260 compare(wizard.shown, true);
12371261 compare(tutorial.running, true);
12381262
1239− // And make sure we stay running when dash focused again
1240− ApplicationManager.startApplication("unity8-dash");
1241− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1263+ // And make sure we stay running when dash comes back again
1264+ var dashSurfaceId = topLevelSurfaceList.nextId;
1265+ ApplicationManager.startApplication(dashApplication.appId);
1266+ waitUntilAppWindowIsFullyLoaded(dashSurfaceId);
12421267 compare(wizard.shown, true);
12431268 compare(tutorial.running, true);
12441269
1245− // And make sure we stop when something else is focused
1246− ApplicationManager.startApplication("gallery-app");
1247− tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
1270+ // And make sure we stop when some other surface shows app
1271+ var gallerySurfaceId = topLevelSurfaceList.nextId;
1272+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1273+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
1274+ tryCompareFunction(function() { return topLevelSurfaceList.applicationAt(0).appId; }, "gallery-app");
12481275 compare(wizard.shown, false);
12491276 compare(tutorial.running, false);
12501277 tryCompare(AccountsService, "demoEdges", false);
@@ -1269,7 +1296,7 @@ Rectangle {
12691296 function test_tapOnRightEdgeReachesApplicationSurface() {
12701297 loadShell("phone");
12711298 swipeAwayGreeter();
1272− var topmostSpreadDelegate = findChild(shell, "appDelegate0");
1299+ var topmostSpreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
12731300 verify(topmostSpreadDelegate);
12741301
12751302 waitUntilFocusedApplicationIsShowingItsSurface();
@@ -1298,7 +1325,7 @@ Rectangle {
12981325 function test_rightEdgeDragDoesNotReachApplicationSurface() {
12991326 loadShell("phone");
13001327 swipeAwayGreeter();
1301− var topmostSpreadDelegate = findChild(shell, "appDelegate0");
1328+ var topmostSpreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
13021329 var topmostSurfaceItem = findChild(topmostSpreadDelegate, "surfaceItem");
13031330 var rightEdgeDragArea = findChild(shell, "spreadDragArea");
13041331
@@ -1318,7 +1345,7 @@ Rectangle {
13181345
13191346 function waitUntilFocusedApplicationIsShowingItsSurface()
13201347 {
1321− var spreadDelegate = findChild(shell, "appDelegate0");
1348+ var spreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
13221349 var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
13231350 tryCompare(appState, "state", "surface");
13241351 var transitions = appState.transitions;
@@ -1467,11 +1494,13 @@ Rectangle {
14671494 loadShell("tablet");
14681495 shell.usageScenario = "desktop";
14691496
1497+ var webBrowserSurfaceId = topLevelSurfaceList.nextId;
14701498 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1471− waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1499+ waitUntilAppWindowIsFullyLoaded(webBrowserSurfaceId);
14721500
1501+ var gallerySurfaceId = topLevelSurfaceList.nextId;
14731502 var galleryApp = ApplicationManager.startApplication("gallery-app");
1474− waitUntilAppWindowIsFullyLoaded(galleryApp);
1503+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
14751504
14761505 ApplicationManager.requestFocusApplication("unity8-dash");
14771506 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
@@ -1489,11 +1518,13 @@ Rectangle {
14891518 loadShell("tablet");
14901519 shell.usageScenario = "tablet";
14911520
1521+ var webBrowserSurfaceId = topLevelSurfaceList.nextId;
14921522 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1493− waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1523+ waitUntilAppWindowIsFullyLoaded(webBrowserSurfaceId);
14941524
1525+ var gallerySurfaceId = topLevelSurfaceList.nextId;
14951526 var galleryApp = ApplicationManager.startApplication("gallery-app");
1496− waitUntilAppWindowIsFullyLoaded(galleryApp);
1527+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
14971528
14981529 ApplicationManager.requestFocusApplication("unity8-dash");
14991530 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
@@ -1523,17 +1554,26 @@ Rectangle {
15231554 var desktopStage = findChild(shell, "stage");
15241555 verify(desktopStage != null)
15251556
1557+ var app1SurfaceId = topLevelSurfaceList.nextId;
15261558 var app1 = ApplicationManager.startApplication("dialer-app")
1527− waitUntilAppWindowIsFullyLoaded(app1);
1559+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
1560+
1561+ var app2SurfaceId = topLevelSurfaceList.nextId;
15281562 var app2 = ApplicationManager.startApplication("webbrowser-app")
1529− waitUntilAppWindowIsFullyLoaded(app2);
1563+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
1564+ var app2Surface = app2.surfaceList.get(0);
1565+ verify(app2Surface);
1566+
1567+ var app3SurfaceId = topLevelSurfaceList.nextId;
15301568 var app3 = ApplicationManager.startApplication("camera-app")
1531− waitUntilAppWindowIsFullyLoaded(app3);
1569+ waitUntilAppWindowIsFullyLoaded(app3SurfaceId);
1570+ var app3Surface = app3.surfaceList.get(0);
1571+ verify(app3Surface);
15321572
15331573 // Do a quick alt-tab and see if focus changes
1534− tryCompare(app3.session.lastSurface, "activeFocus", true)
1574+ tryCompare(app3Surface, "activeFocus", true)
15351575 keyClick(Qt.Key_Tab, Qt.AltModifier)
1536− tryCompare(app2.session.lastSurface, "activeFocus", true)
1576+ tryCompare(app2Surface, "activeFocus", true)
15371577
15381578 // Press Alt+Tab
15391579 keyPress(Qt.Key_Alt);
@@ -1541,7 +1581,7 @@ Rectangle {
15411581 keyRelease(Qt.Key_Alt)
15421582
15431583 // Focus should have switched back now
1544− tryCompare(app3.session.lastSurface, "activeFocus", true)
1584+ tryCompare(app3Surface, "activeFocus", true)
15451585 }
15461586
15471587 function test_altTabWrapAround() {
@@ -1590,7 +1630,9 @@ Rectangle {
15901630 tryCompare(desktopSpread, "state", "")
15911631
15921632 // Make sure that after wrapping around once, we have the same one focused as at the beginning
1593− tryCompare(focused.session.lastSurface, "activeFocus", true)
1633+ var focusedAppSurface = focused.surfaceList.get(0);
1634+ verify(focusedAppSurface);
1635+ tryCompare(focusedAppSurface, "activeFocus", true)
15941636 }
15951637
15961638 function test_altBackTabNavigation() {
@@ -1624,7 +1666,7 @@ Rectangle {
16241666 keyRelease(Qt.Key_Alt);
16251667 }
16261668
1627− function test_highlightFollowsMouse() {
1669+ function otest_highlightFollowsMouse() {
16281670 loadDesktopShellWithApps()
16291671
16301672 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
@@ -1661,9 +1703,7 @@ Rectangle {
16611703 keyPress(Qt.Key_Alt)
16621704 keyClick(Qt.Key_Tab);
16631705
1664− appRemovedSpy.clear();
1665−
1666− var closedAppId = ApplicationManager.get(2).appId;
1706+ var surfaceId = topLevelSurfaceList.idAt(2);
16671707 var spreadDelegate2 = spreadRepeater.itemAt(2);
16681708 var closeMouseArea = findChild(spreadDelegate2, "closeMouseArea");
16691709
@@ -1678,12 +1718,14 @@ Rectangle {
16781718 }
16791719 tryCompare(closeMouseArea, "enabled", true)
16801720
1721+ var countBeforeClickingCloseButton = topLevelSurfaceList.count;
1722+ verify(topLevelSurfaceList.indexForId(surfaceId) === 2);
1723+
16811724 // Close the app using the close button
16821725 mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
16831726
1684− // Verify applicationRemoved has been emitted correctly
1685− tryCompare(appRemovedSpy, "count", 1)
1686− compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1727+ tryCompare(topLevelSurfaceList, "count", countBeforeClickingCloseButton - 1);
1728+ verify(topLevelSurfaceList.indexForId(surfaceId) === -1);
16871729
16881730 keyRelease(Qt.Key_Alt);
16891731 }
@@ -1708,7 +1750,7 @@ Rectangle {
17081750 keyPress(Qt.Key_Alt)
17091751 keyClick(Qt.Key_Tab);
17101752
1711− var focusAppId = ApplicationManager.get(2).appId;
1753+ var surface = topLevelSurfaceList.surfaceAt(2);
17121754 var spreadDelegate2 = spreadRepeater.itemAt(2);
17131755 var clippedSpreadDelegate = findChild(spreadDelegate2, "clippedSpreadDelegate");
17141756
@@ -1730,7 +1772,7 @@ Rectangle {
17301772
17311773 // Verify that we left the spread and app2 is the focused one now
17321774 tryCompare(stage, "state", "");
1733− tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1775+ tryCompare(surface, "focused", true);
17341776
17351777 keyRelease(Qt.Key_Alt);
17361778 }
@@ -1794,7 +1836,7 @@ Rectangle {
17941836 function test_workspacePreviewsHighlightedApp() {
17951837 loadDesktopShellWithApps()
17961838
1797− var targetZ = ApplicationManager.count + 1;
1839+ var targetZ = topLevelSurfaceList.count + 1;
17981840
17991841 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
18001842 verify(spreadRepeater !== null);
@@ -1870,7 +1912,8 @@ Rectangle {
18701912 loadDesktopShellWithApps();
18711913
18721914 var appRepeater = findChild(shell, "appRepeater")
1873− var appId = ApplicationManager.get(0).appId;
1915+ var application = topLevelSurfaceList.applicationAt(0);
1916+ var surfaceId = topLevelSurfaceList.idAt(0);
18741917 var appDelegate = appRepeater.itemAt(0);
18751918 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
18761919
@@ -1881,17 +1924,20 @@ Rectangle {
18811924 tryCompare(appDelegate, "state", "maximized");
18821925 tryCompare(PanelState, "buttonsVisible", true)
18831926
1884− ApplicationManager.stopApplication(appId);
1927+ ApplicationManager.stopApplication(application.appId);
18851928 tryCompare(PanelState, "buttonsVisible", false)
18861929
1887− ApplicationManager.startApplication(appId);
1930+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
1931+ // They won't be gone until those surface items are destroyed.
1932+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
1933+
1934+ ApplicationManager.startApplication(application.appId);
18881935 tryCompare(PanelState, "buttonsVisible", true)
18891936 }
18901937
18911938 function test_newAppHasValidGeometry() {
18921939 loadDesktopShellWithApps();
18931940 var appRepeater = findChild(shell, "appRepeater");
1894− var appId = ApplicationManager.get(0).appId;
18951941 var appDelegate = appRepeater.itemAt(0);
18961942
18971943 var resizeArea = findChild(appDelegate, "windowResizeArea");
@@ -1908,7 +1954,6 @@ Rectangle {
19081954 function test_switchToStagedHidesPanelButtons() {
19091955 loadDesktopShellWithApps();
19101956 var appRepeater = findChild(shell, "appRepeater")
1911− var appId = ApplicationManager.get(0).appId;
19121957 var appDelegate = appRepeater.itemAt(0);
19131958 var panelButtons = findChild(shell, "panelWindowControlButtons")
19141959
@@ -1925,7 +1970,6 @@ Rectangle {
19251970 function test_lockingGreeterHidesPanelButtons() {
19261971 loadDesktopShellWithApps();
19271972 var appRepeater = findChild(shell, "appRepeater")
1928− var appId = ApplicationManager.get(0).appId;
19291973 var appDelegate = appRepeater.itemAt(0);
19301974 var panelButtons = findChild(shell, "panelWindowControlButtons")
19311975
@@ -1955,12 +1999,15 @@ Rectangle {
19551999 shell.usageScenario = "desktop";
19562000 waitForRendering(shell);
19572001
2002+ var appSurfaceId = topLevelSurfaceList.nextId;
19582003 var app = ApplicationManager.startApplication("dialer-app")
1959− waitUntilAppWindowIsFullyLoaded(app);
2004+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
19602005
19612006 var appContainer = findChild(shell, "appContainer");
1962− var appDelegate = findChild(appContainer, "appDelegate_dialer-app");
1963− var decoration = findChild(appDelegate, "appWindowDecoration_dialer-app");
2007+ verify(appContainer);
2008+ var appDelegate = findChild(appContainer, "appDelegate_" + appSurfaceId);
2009+ verify(appDelegate);
2010+ var decoration = findChild(appDelegate, "appWindowDecoration");
19642011 verify(decoration);
19652012
19662013 // move it away from launcher and panel
@@ -1978,18 +2025,22 @@ Rectangle {
19782025 function test_restoreWindowStateFixesIfUnderPanel() {
19792026 loadDesktopShellWithApps();
19802027 var appRepeater = findChild(shell, "appRepeater")
1981− var appId = ApplicationManager.get(0).appId;
2028+ var application = topLevelSurfaceList.applicationAt(0);
19822029 var appDelegate = appRepeater.itemAt(0);
19832030
19842031 // Move it under the panel programmatically (might happen later with an alt+drag)
19852032 appDelegate.y = -units.gu(10)
19862033
1987− ApplicationManager.stopApplication(appId)
1988− ApplicationManager.startApplication(appId)
2034+ ApplicationManager.stopApplication(application.appId);
2035+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
2036+ // They won't be gone until those surface items are destroyed.
2037+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
2038+
2039+ ApplicationManager.startApplication(application.appId);
19892040 waitForRendering(shell)
19902041
19912042 // Make sure the newly started one is at index 0 again
1992− tryCompare(ApplicationManager.get(0), "appId", appId);
2043+ tryCompare(topLevelSurfaceList.applicationAt(0), "appId", application.appId);
19932044
19942045 appDelegate = appRepeater.itemAt(0);
19952046 compare(appDelegate.y >= PanelState.panelHeight, true);
@@ -2008,16 +2059,22 @@ Rectangle {
20082059
20092060 // Add two main stage apps, the second in order to suspend the first.
20102061 // LibreOffice has isTouchApp set to false by our mocks.
2062+ var app1SurfaceId = topLevelSurfaceList.nextId;
20112063 var app1 = ApplicationManager.startApplication("libreoffice");
2012− waitUntilAppWindowIsFullyLoaded(app1);
2064+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
2065+ var app2SurfaceId = topLevelSurfaceList.nextId;
20132066 var app2 = ApplicationManager.startApplication("gallery-app");
2014− waitUntilAppWindowIsFullyLoaded(app2);
2067+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
20152068
20162069 // Sanity checking
20172070 compare(app1.stage, ApplicationInfoInterface.MainStage);
20182071 compare(app2.stage, ApplicationInfoInterface.MainStage);
20192072 verify(!app1.isTouchApp);
2020− verify(!app1.session.lastSurface.activeFocus);
2073+
2074+ var app1Surface = app1.surfaceList.get(0);
2075+ verify(app1Surface);
2076+
2077+ verify(!app1Surface.activeFocus);
20212078
20222079 // Make sure app1 is exempt with a requested suspend
20232080 verify(app1.exemptFromLifecycle);
@@ -2038,15 +2095,21 @@ Rectangle {
20382095 GSettingsController.setLifecycleExemptAppids(["webbrowser-app"]);
20392096
20402097 // Add two main stage apps, the second in order to suspend the first
2098+ var app1SurfaceId = topLevelSurfaceList.nextId;
20412099 var app1 = ApplicationManager.startApplication("webbrowser-app");
2042− waitUntilAppWindowIsFullyLoaded(app1);
2100+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
2101+ var app2SurfaceId = topLevelSurfaceList.nextId;
20432102 var app2 = ApplicationManager.startApplication("gallery-app");
2044− waitUntilAppWindowIsFullyLoaded(app2);
2103+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
20452104
20462105 // Sanity checking
20472106 compare(app1.stage, ApplicationInfoInterface.MainStage);
20482107 compare(app2.stage, ApplicationInfoInterface.MainStage);
2049− verify(!app1.session.lastSurface.activeFocus);
2108+
2109+ var app1Surface = app1.surfaceList.get(0);
2110+ verify(app1Surface);
2111+
2112+ verify(!app1Surface.activeFocus);
20502113
20512114 // Make sure app1 is exempt with a requested suspend
20522115 verify(app1.exemptFromLifecycle);
@@ -2205,9 +2268,10 @@ Rectangle {
22052268 var appContainer = findChild(shell, "appContainer");
22062269 var launcher = findChild(shell, "launcher");
22072270
2271+ var appSurfaceId = topLevelSurfaceList.nextId;
22082272 var app = ApplicationManager.startApplication("music-app");
2209− waitUntilAppWindowIsFullyLoaded(app);
2210− var appDelegate = findChild(appContainer, "appDelegate_music-app");
2273+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
2274+ var appDelegate = findChild(appContainer, "appDelegate_" + appSurfaceId);
22112275 appDelegate.maximize();
22122276 tryCompare(appDelegate, "visuallyMaximized", true);
22132277 waitForRendering(shell);
@@ -2235,8 +2299,9 @@ Rectangle {
22352299
22362300 tryCompare(launcher, "lockedVisible", true);
22372301
2302+ var cameraSurfaceId = topLevelSurfaceList.nextId;
22382303 var cameraApp = ApplicationManager.startApplication("camera-app");
2239− waitUntilAppWindowIsFullyLoaded(cameraApp);
2304+ waitUntilAppWindowIsFullyLoaded(cameraSurfaceId);
22402305
22412306 tryCompare(launcher, "lockedVisible", false);
22422307 }
@@ -2257,8 +2322,9 @@ Rectangle {
22572322 swipeAwayGreeter();
22582323
22592324 // Let's open a fullscreen app
2325+ var appSurfaceId = topLevelSurfaceList.nextId;
22602326 var app = ApplicationManager.startApplication("camera-app");
2261− waitUntilAppWindowIsFullyLoaded(app);
2327+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
22622328
22632329 var appRepeater = findChild(shell, data.repeaterName);
22642330 var topmostAppDelegate = appRepeater.itemAt(0);
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}