on xenial help sign in
Surface-based window management
fdf1715Daniel d'Andrada
debian/control+1−1
@@ -30,7 +30,7 @@ Build-Depends: cmake,
30 libqt5xmlpatterns5-dev,
30 libqt5xmlpatterns5-dev,
31 libsystemsettings-dev,
31 libsystemsettings-dev,
32 libudev-dev,
32 libudev-dev,
33− libunity-api-dev (>= 7.108),
33+ libunity-api-dev (>= 7.110),
34 libusermetricsoutput1-dev,
34 libusermetricsoutput1-dev,
35# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
35# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
36 libx11-dev[!armhf],
36 libx11-dev[!armhf],
debian/unity8-private.install+1
@@ -14,6 +14,7 @@ usr/lib/*/unity8/qml/Ubuntu
14usr/lib/*/unity8/qml/Unity
14usr/lib/*/unity8/qml/Unity
15usr/lib/*/unity8/qml/UInput
15usr/lib/*/unity8/qml/UInput
16usr/lib/*/unity8/qml/Utils
16usr/lib/*/unity8/qml/Utils
17+usr/lib/*/unity8/qml/WindowManager
17usr/lib/*/unity8/qml/Wizard
18usr/lib/*/unity8/qml/Wizard
18usr/share/accountsservice/interfaces
19usr/share/accountsservice/interfaces
19usr/share/dbus-1/interfaces
20usr/share/dbus-1/interfaces
plugins/CMakeLists.txt+1
@@ -25,4 +25,5 @@ add_subdirectory(Ubuntu)
25add_subdirectory(UInput)
25add_subdirectory(UInput)
26add_subdirectory(Unity)
26add_subdirectory(Unity)
27add_subdirectory(Utils)
27add_subdirectory(Utils)
28+add_subdirectory(WindowManager)
28add_subdirectory(Wizard)
29add_subdirectory(Wizard)
plugins/WindowManager/CMakeLists.txtnew file+13
@@ -0,0 +1,13 @@
1+set(WINDOWMANAGER_SRC
2+ TopLevelSurfaceList.cpp
3+ WindowManagerPlugin.cpp
4+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
5+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
6+ ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
7+ )
8+
9+add_library(windowmanager-qml SHARED ${WINDOWMANAGER_SRC})
10+
11+qt5_use_modules(windowmanager-qml Qml Quick Gui)
12+
13+add_unity8_plugin(WindowManager 0.1 WindowManager TARGETS windowmanager-qml)
plugins/WindowManager/TopLevelSurfaceList.cppnew file+486
@@ -0,0 +1,486 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "TopLevelSurfaceList.h"
18+
19+// unity-api
20+#include <unity/shell/application/ApplicationInfoInterface.h>
21+#include <unity/shell/application/MirSurfaceInterface.h>
22+#include <unity/shell/application/MirSurfaceListInterface.h>
23+
24+#include <QMetaObject>
25+
26+Q_LOGGING_CATEGORY(UNITY_TOPSURFACELIST, "unity.topsurfacelist", QtDebugMsg)
27+
28+#define DEBUG_MSG qCDebug(UNITY_TOPSURFACELIST).nospace().noquote() << __func__
29+
30+using namespace unity::shell::application;
31+
32+TopLevelSurfaceList::TopLevelSurfaceList(QObject *parent) :
33+ QAbstractListModel(parent)
34+{
35+ DEBUG_MSG << "()";
36+}
37+
38+TopLevelSurfaceList::~TopLevelSurfaceList()
39+{
40+ DEBUG_MSG << "()";
41+}
42+
43+int TopLevelSurfaceList::rowCount(const QModelIndex &parent) const
44+{
45+ return !parent.isValid() ? m_surfaceList.size() : 0;
46+}
47+
48+QVariant TopLevelSurfaceList::data(const QModelIndex& index, int role) const
49+{
50+ if (index.row() < 0 || index.row() >= m_surfaceList.size())
51+ return QVariant();
52+
53+ if (role == SurfaceRole) {
54+ MirSurfaceInterface *surface = m_surfaceList.at(index.row()).surface;
55+ return QVariant::fromValue(surface);
56+ } else if (role == ApplicationRole) {
57+ return QVariant::fromValue(m_surfaceList.at(index.row()).application);
58+ } else if (role == IdRole) {
59+ return QVariant::fromValue(m_surfaceList.at(index.row()).id);
60+ } else {
61+ return QVariant();
62+ }
63+}
64+
65+void TopLevelSurfaceList::raise(MirSurfaceInterface *surface)
66+{
67+ if (!surface)
68+ return;
69+
70+ DEBUG_MSG << "(MirSurface[" << (void*)surface << "])";
71+
72+ int i = indexOf(surface);
73+ if (i != -1) {
74+ raiseId(m_surfaceList.at(i).id);
75+ }
76+}
77+
78+void TopLevelSurfaceList::appendPlaceholder(ApplicationInfoInterface *application)
79+{
80+ DEBUG_MSG << "(" << application->appId() << ")";
81+
82+ appendSurfaceHelper(nullptr, application);
83+}
84+
85+void TopLevelSurfaceList::appendSurface(MirSurfaceInterface *surface, ApplicationInfoInterface *application)
86+{
87+ Q_ASSERT(surface != nullptr);
88+
89+ bool filledPlaceholder = false;
90+ for (int i = 0; i < m_surfaceList.count() && !filledPlaceholder; ++i) {
91+ ModelEntry &entry = m_surfaceList[i];
92+ if (entry.application == application && entry.surface == nullptr) {
93+ entry.surface = surface;
94+ connectSurface(surface);
95+ DEBUG_MSG << " appId=" << application->appId() << " surface=" << surface
96+ << ", filling out placeholder. after: " << toString();
97+ Q_EMIT dataChanged(index(i) /* topLeft */, index(i) /* bottomRight */, QVector<int>() << SurfaceRole);
98+ filledPlaceholder = true;
99+ }
100+ }
101+
102+ if (!filledPlaceholder) {
103+ DEBUG_MSG << " appId=" << application->appId() << " surface=" << surface << ", adding new row";
104+ appendSurfaceHelper(surface, application);
105+ }
106+}
107+
108+void TopLevelSurfaceList::appendSurfaceHelper(MirSurfaceInterface *surface, ApplicationInfoInterface *application)
109+{
110+ if (m_modelState == IdleState) {
111+ m_modelState = InsertingState;
112+ beginInsertRows(QModelIndex(), m_surfaceList.size() /*first*/, m_surfaceList.size() /*last*/);
113+ } else {
114+ Q_ASSERT(m_modelState == ResettingState);
115+ // No point in signaling anything if we're resetting the whole model
116+ }
117+
118+ int id = generateId();
119+ m_surfaceList.append(ModelEntry(surface, application, id));
120+ if (surface) {
121+ connectSurface(surface);
122+ }
123+
124+ if (m_modelState == InsertingState) {
125+ endInsertRows();
126+ Q_EMIT countChanged();
127+ Q_EMIT listChanged();
128+ m_modelState = IdleState;
129+ }
130+
131+ DEBUG_MSG << " after " << toString();
132+}
133+
134+void TopLevelSurfaceList::connectSurface(MirSurfaceInterface *surface)
135+{
136+ connect(surface, &MirSurfaceInterface::focusedChanged, this, [this, surface](bool focused){
137+ if (focused) {
138+ this->raise(surface);
139+ }
140+ });
141+ connect(surface, &MirSurfaceInterface::liveChanged, this, [this, surface](bool live){
142+ if (!live) {
143+ onSurfaceDied(surface);
144+ }
145+ });
146+ connect(surface, &QObject::destroyed, this, [this, surface](){ this->onSurfaceDestroyed(surface); });
147+}
148+
149+void TopLevelSurfaceList::onSurfaceDied(MirSurfaceInterface *surface)
150+{
151+ int i = indexOf(surface);
152+ if (i == -1) {
153+ return;
154+ }
155+
156+ auto application = m_surfaceList[i].application;
157+
158+ // can't be starting if it already has a surface
159+ Q_ASSERT(application->state() != ApplicationInfoInterface::Starting);
160+
161+ if (application->state() == ApplicationInfoInterface::Running) {
162+ m_surfaceList[i].removeOnceSurfaceDestroyed = true;
163+ } else {
164+ // assume it got killed by the out-of-memory daemon.
165+ //
166+ // So leave entry in the model and only remove its surface, so shell can display a screenshot
167+ // in its place.
168+ m_surfaceList[i].removeOnceSurfaceDestroyed = false;
169+ }
170+}
171+
172+void TopLevelSurfaceList::onSurfaceDestroyed(MirSurfaceInterface *surface)
173+{
174+ int i = indexOf(surface);
175+ if (i == -1) {
176+ return;
177+ }
178+
179+ if (m_surfaceList[i].removeOnceSurfaceDestroyed) {
180+ removeAt(i);
181+ } else {
182+ m_surfaceList[i].surface = nullptr;
183+ Q_EMIT dataChanged(index(i) /* topLeft */, index(i) /* bottomRight */, QVector<int>() << SurfaceRole);
184+ DEBUG_MSG << " Removed surface from entry. After: " << toString();
185+ }
186+}
187+
188+void TopLevelSurfaceList::removeAt(int index)
189+{
190+ if (m_modelState == IdleState) {
191+ beginRemoveRows(QModelIndex(), index, index);
192+ m_modelState = RemovingState;
193+ } else {
194+ Q_ASSERT(m_modelState == ResettingState);
195+ // No point in signaling anything if we're resetting the whole model
196+ }
197+
198+ m_surfaceList.removeAt(index);
199+
200+ if (m_modelState == RemovingState) {
201+ endRemoveRows();
202+ Q_EMIT countChanged();
203+ Q_EMIT listChanged();
204+ m_modelState = IdleState;
205+ }
206+
207+ DEBUG_MSG << " after " << toString();
208+}
209+
210+int TopLevelSurfaceList::indexOf(MirSurfaceInterface *surface)
211+{
212+ for (int i = 0; i < m_surfaceList.count(); ++i) {
213+ if (m_surfaceList.at(i).surface == surface) {
214+ return i;
215+ }
216+ }
217+ return -1;
218+}
219+
220+void TopLevelSurfaceList::move(int from, int to)
221+{
222+ if (from == to) return;
223+ DEBUG_MSG << " from=" << from << " to=" << to;
224+
225+ if (from >= 0 && from < m_surfaceList.size() && to >= 0 && to < m_surfaceList.size()) {
226+ QModelIndex parent;
227+ /* When moving an item down, the destination index needs to be incremented
228+ by one, as explained in the documentation:
229+ http://qt-project.org/doc/qt-5.0/qtcore/qabstractitemmodel.html#beginMoveRows */
230+
231+ Q_ASSERT(m_modelState == IdleState);
232+ m_modelState = MovingState;
233+
234+ beginMoveRows(parent, from, from, parent, to + (to > from ? 1 : 0));
235+ m_surfaceList.move(from, to);
236+ endMoveRows();
237+ Q_EMIT listChanged();
238+
239+ m_modelState = IdleState;
240+
241+ DEBUG_MSG << " after " << toString();
242+ }
243+}
244+
245+MirSurfaceInterface *TopLevelSurfaceList::surfaceAt(int index) const
246+{
247+ if (index >=0 && index < m_surfaceList.count()) {
248+ return m_surfaceList[index].surface;
249+ } else {
250+ return nullptr;
251+ }
252+}
253+
254+ApplicationInfoInterface *TopLevelSurfaceList::applicationAt(int index) const
255+{
256+ if (index >=0 && index < m_surfaceList.count()) {
257+ return m_surfaceList[index].application;
258+ } else {
259+ return nullptr;
260+ }
261+}
262+
263+int TopLevelSurfaceList::idAt(int index) const
264+{
265+ if (index >=0 && index < m_surfaceList.count()) {
266+ return m_surfaceList[index].id;
267+ } else {
268+ return 0;
269+ }
270+}
271+
272+int TopLevelSurfaceList::indexForId(int id) const
273+{
274+ for (int i = 0; i < m_surfaceList.count(); ++i) {
275+ if (m_surfaceList[i].id == id) {
276+ return i;
277+ }
278+ }
279+ return -1;
280+}
281+
282+void TopLevelSurfaceList::doRaiseId(int id)
283+{
284+ int fromIndex = indexForId(id);
285+ if (fromIndex != -1) {
286+ move(fromIndex, 0 /* toIndex */);
287+ }
288+}
289+
290+void TopLevelSurfaceList::raiseId(int id)
291+{
292+ if (m_modelState == IdleState) {
293+ DEBUG_MSG << "(id=" << id << ") - do it now.";
294+ doRaiseId(id);
295+ } else {
296+ DEBUG_MSG << "(id=" << id << ") - Model busy (modelState=" << m_modelState << "). Try again in the next event loop.";
297+ // The model has just signalled some change. If we have a Repeater responding to this update, it will get nuts
298+ // if we perform yet another model change straight away.
299+ //
300+ // A bad sympton of this problem is a Repeater.itemAt(index) call returning null event though Repeater.count says
301+ // the index is definitely within bounds.
302+ QMetaObject::invokeMethod(this, "raiseId", Qt::QueuedConnection, Q_ARG(int, id));
303+ }
304+}
305+
306+int TopLevelSurfaceList::generateId()
307+{
308+ int id = m_nextId;
309+ m_nextId = nextFreeId(m_nextId + 1);
310+ Q_EMIT nextIdChanged();
311+ return id;
312+}
313+
314+int TopLevelSurfaceList::nextFreeId(int candidateId)
315+{
316+ if (candidateId > m_maxId) {
317+ return nextFreeId(1);
318+ } else {
319+ if (indexForId(candidateId) == -1) {
320+ // it's indeed free
321+ return candidateId;
322+ } else {
323+ return nextFreeId(candidateId + 1);
324+ }
325+ }
326+}
327+
328+QString TopLevelSurfaceList::toString()
329+{
330+ QString str;
331+ for (int i = 0; i < m_surfaceList.count(); ++i) {
332+ auto item = m_surfaceList.at(i);
333+
334+ QString itemStr = QString("(index=%1,appId=%2,surface=0x%3,id=%4)")
335+ .arg(i)
336+ .arg(item.application->appId())
337+ .arg((qintptr)item.surface, 0, 16)
338+ .arg(item.id);
339+
340+ if (i > 0) {
341+ str.append(",");
342+ }
343+ str.append(itemStr);
344+ }
345+ return str;
346+}
347+
348+void TopLevelSurfaceList::addApplication(ApplicationInfoInterface *application)
349+{
350+ DEBUG_MSG << "(" << application->appId() << ")";
351+ Q_ASSERT(!m_applications.contains(application));
352+ m_applications.append(application);
353+
354+ MirSurfaceListInterface *surfaceList = application->surfaceList();
355+
356+ if (application->state() != ApplicationInfoInterface::Stopped) {
357+ if (surfaceList->count() == 0) {
358+ appendPlaceholder(application);
359+ } else {
360+ for (int i = 0; i < surfaceList->count(); ++i) {
361+ appendSurface(surfaceList->get(i), application);
362+ }
363+ }
364+ }
365+
366+ connect(surfaceList, &QAbstractItemModel::rowsInserted, this,
367+ [this, application, surfaceList](const QModelIndex & /*parent*/, int first, int last)
368+ {
369+ for (int i = last; i >= first; --i) {
370+ this->appendSurface(surfaceList->get(i), application);
371+ }
372+ });
373+}
374+
375+void TopLevelSurfaceList::removeApplication(ApplicationInfoInterface *application)
376+{
377+ DEBUG_MSG << "(" << application->appId() << ")";
378+ Q_ASSERT(m_applications.contains(application));
379+
380+ MirSurfaceListInterface *surfaceList = application->surfaceList();
381+
382+ disconnect(surfaceList, 0, this, 0);
383+
384+ Q_ASSERT(m_modelState == IdleState);
385+ m_modelState = RemovingState;
386+
387+ int i = 0;
388+ while (i < m_surfaceList.count()) {
389+ if (m_surfaceList.at(i).application == application) {
390+ beginRemoveRows(QModelIndex(), i, i);
391+ m_surfaceList.removeAt(i);
392+ endRemoveRows();
393+ Q_EMIT countChanged();
394+ Q_EMIT listChanged();
395+ } else {
396+ ++i;
397+ }
398+ }
399+
400+ m_modelState = IdleState;
401+
402+ DEBUG_MSG << " after " << toString();
403+
404+ m_applications.removeAll(application);
405+}
406+
407+QAbstractListModel *TopLevelSurfaceList::applicationsModel() const
408+{
409+ return m_applicationsModel;
410+}
411+
412+void TopLevelSurfaceList::setApplicationsModel(QAbstractListModel* value)
413+{
414+ if (m_applicationsModel == value) {
415+ return;
416+ }
417+
418+ DEBUG_MSG << "(" << value << ")";
419+
420+ Q_ASSERT(m_modelState == IdleState);
421+ m_modelState = ResettingState;
422+
423+ beginResetModel();
424+
425+ if (m_applicationsModel) {
426+ m_surfaceList.clear();
427+ m_applications.clear();
428+ disconnect(m_applicationsModel, 0, this, 0);
429+ }
430+
431+ m_applicationsModel = value;
432+
433+ if (m_applicationsModel) {
434+ findApplicationRole();
435+
436+ connect(m_applicationsModel, &QAbstractItemModel::rowsInserted,
437+ this, [this](const QModelIndex &/*parent*/, int first, int last) {
438+ for (int i = first; i <= last; ++i) {
439+ auto application = getApplicationFromModelAt(i);
440+ addApplication(application);
441+ }
442+ });
443+
444+ connect(m_applicationsModel, &QAbstractItemModel::rowsAboutToBeRemoved,
445+ this, [this](const QModelIndex &/*parent*/, int first, int last) {
446+ for (int i = first; i <= last; ++i) {
447+ auto application = getApplicationFromModelAt(i);
448+ removeApplication(application);
449+ }
450+ });
451+
452+ for (int i = 0; i < m_applicationsModel->rowCount(); ++i) {
453+ auto application = getApplicationFromModelAt(i);
454+ addApplication(application);
455+ }
456+ }
457+
458+ endResetModel();
459+ m_modelState = IdleState;
460+}
461+
462+ApplicationInfoInterface *TopLevelSurfaceList::getApplicationFromModelAt(int index)
463+{
464+ QModelIndex modelIndex = m_applicationsModel->index(index);
465+
466+ QVariant variant = m_applicationsModel->data(modelIndex, m_applicationRole);
467+
468+ // variant.value<ApplicationInfoInterface*>() returns null for some reason.
469+ return static_cast<ApplicationInfoInterface*>(variant.value<QObject*>());
470+}
471+
472+void TopLevelSurfaceList::findApplicationRole()
473+{
474+ QHash<int, QByteArray> namesHash = m_applicationsModel->roleNames();
475+
476+ m_applicationRole = -1;
477+ for (auto i = namesHash.begin(); i != namesHash.end() && m_applicationRole == -1; ++i) {
478+ if (i.value() == "application") {
479+ m_applicationRole = i.key();
480+ }
481+ }
482+
483+ if (m_applicationRole == -1) {
484+ qFatal("TopLevelSurfaceList: applicationsModel must have a \"application\" role.");
485+ }
486+}
plugins/WindowManager/TopLevelSurfaceList.hnew file+223
@@ -0,0 +1,223 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef TOPLEVELSURFACELIST_H
18+#define TOPLEVELSURFACELIST_H
19+
20+#include <QAbstractListModel>
21+#include <QList>
22+#include <QLoggingCategory>
23+
24+Q_DECLARE_LOGGING_CATEGORY(UNITY_TOPSURFACELIST)
25+
26+namespace unity {
27+ namespace shell {
28+ namespace application {
29+ class ApplicationInfoInterface;
30+ class MirSurfaceInterface;
31+ }
32+ }
33+}
34+
35+/**
36+ * @brief A model of top-level surfaces
37+ *
38+ * It's an abstraction of top-level application windows.
39+ *
40+ * When an entry first appears, it normaly doesn't have a surface yet, meaning that the application is
41+ * still starting up. A shell should then display a splash screen or saved screenshot of the application
42+ * until its surface comes up.
43+ *
44+ * As applications can have multiple surfaces and you can also have entries without surfaces at all,
45+ * the only way to unambiguously refer to an entry in this model is through its id.
46+ */
47+class TopLevelSurfaceList : public QAbstractListModel
48+{
49+
50+ Q_OBJECT
51+
52+ /**
53+ * @brief A list model of applications.
54+ *
55+ * It's expected to have a role called "application" which returns a ApplicationInfoInterface
56+ */
57+ Q_PROPERTY(QAbstractListModel* applicationsModel READ applicationsModel
58+ WRITE setApplicationsModel
59+ NOTIFY applicationsModelChanged)
60+
61+ /**
62+ * @brief Number of top-level surfaces in this model
63+ *
64+ * This is the same as rowCount, added in order to keep compatibility with QML ListModels.
65+ */
66+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
67+
68+ /**
69+ The id to be used on the next entry created
70+ Useful for tests
71+ */
72+ Q_PROPERTY(int nextId READ nextId NOTIFY nextIdChanged)
73+public:
74+
75+ /**
76+ * @brief The Roles supported by the model
77+ *
78+ * SurfaceRole - A MirSurfaceInterface. It will be null if the application is still starting up
79+ * ApplicationRole - An ApplicationInfoInterface
80+ * IdRole - A unique identifier for this entry. Useful to unambiguosly track elements as they move around in the list
81+ */
82+ enum Roles {
83+ SurfaceRole = Qt::UserRole,
84+ ApplicationRole = Qt::UserRole + 1,
85+ IdRole = Qt::UserRole + 2,
86+ };
87+
88+ explicit TopLevelSurfaceList(QObject *parent = nullptr);
89+ virtual ~TopLevelSurfaceList();
90+
91+ // QAbstractItemModel methods
92+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
93+ QVariant data(const QModelIndex& index, int role) const override;
94+ QHash<int, QByteArray> roleNames() const override {
95+ QHash<int, QByteArray> roleNames { {SurfaceRole, "surface"},
96+ {ApplicationRole, "application"},
97+ {IdRole, "id"} };
98+ return roleNames;
99+ }
100+
101+ int nextId() const { return m_nextId; }
102+
103+ QAbstractListModel *applicationsModel() const;
104+ void setApplicationsModel(QAbstractListModel*);
105+
106+public Q_SLOTS:
107+ /**
108+ * @brief Returns the surface at the given index
109+ *
110+ * It will be a nullptr if the application is still starting up and thus hasn't yet created
111+ * and drawn into a surface.
112+ */
113+ unity::shell::application::MirSurfaceInterface *surfaceAt(int index) const;
114+
115+ /**
116+ * @brief Returns the application at the given index
117+ */
118+ unity::shell::application::ApplicationInfoInterface *applicationAt(int index) const;
119+
120+ /**
121+ * @brief Returns the unique id of the element at the given index
122+ */
123+ int idAt(int index) const;
124+
125+ /**
126+ * @brief Returns the index where the row with the given id is located
127+ *
128+ * Returns -1 if there's no row with the given id.
129+ */
130+ int indexForId(int id) const;
131+
132+ /**
133+ * @brief Raises the row with the given id to index 0
134+ */
135+ void raiseId(int id);
136+
137+ void doRaiseId(int id);
138+
139+Q_SIGNALS:
140+ void countChanged();
141+
142+ /**
143+ * @brief Emitted when the list changes
144+ *
145+ * Emitted when model gains an element, loses an element or when elements exchange positions.
146+ */
147+ void listChanged();
148+
149+ void nextIdChanged();
150+
151+ void applicationsModelChanged();
152+
153+private:
154+ void addApplication(unity::shell::application::ApplicationInfoInterface *application);
155+ void removeApplication(unity::shell::application::ApplicationInfoInterface *application);
156+
157+ int indexOf(unity::shell::application::MirSurfaceInterface *surface);
158+ void raise(unity::shell::application::MirSurfaceInterface *surface);
159+ void move(int from, int to);
160+ void appendSurfaceHelper(unity::shell::application::MirSurfaceInterface *surface,
161+ unity::shell::application::ApplicationInfoInterface *application);
162+ void connectSurface(unity::shell::application::MirSurfaceInterface *surface);
163+ int generateId();
164+ int nextFreeId(int candidateId);
165+ QString toString();
166+ void onSurfaceDestroyed(unity::shell::application::MirSurfaceInterface *surface);
167+ void onSurfaceDied(unity::shell::application::MirSurfaceInterface *surface);
168+ void removeAt(int index);
169+ void findApplicationRole();
170+
171+ unity::shell::application::ApplicationInfoInterface *getApplicationFromModelAt(int index);
172+
173+ /*
174+ Placeholder for a future surface from a starting or running application.
175+ Enables shell to give immediate feedback to the user by showing, eg,
176+ a splash screen.
177+
178+ It's a model row containing a null surface and the given application.
179+ */
180+ void appendPlaceholder(unity::shell::application::ApplicationInfoInterface *application);
181+
182+ /*
183+ Adds a model row with the given surface and application
184+
185+ Alternatively, if a placeholder exists for the given application it's
186+ filled with the given surface instead.
187+ */
188+ void appendSurface(unity::shell::application::MirSurfaceInterface *surface,
189+ unity::shell::application::ApplicationInfoInterface *application);
190+
191+ struct ModelEntry {
192+ ModelEntry(unity::shell::application::MirSurfaceInterface *surface, unity::shell::application::ApplicationInfoInterface *application, int id)
193+ : surface(surface), application(application), id(id) {}
194+ unity::shell::application::MirSurfaceInterface *surface;
195+ unity::shell::application::ApplicationInfoInterface *application;
196+ int id;
197+ bool removeOnceSurfaceDestroyed{false};
198+ };
199+
200+ QList<ModelEntry> m_surfaceList;
201+ int m_nextId{1};
202+ static const int m_maxId{1000000};
203+
204+ // applications that are being monitored
205+ QList<unity::shell::application::ApplicationInfoInterface *> m_applications;
206+
207+ QAbstractListModel* m_applicationsModel{nullptr};
208+ int m_applicationRole{-1};
209+
210+ enum ModelState {
211+ IdleState,
212+ InsertingState,
213+ RemovingState,
214+ MovingState,
215+ ResettingState
216+ };
217+ ModelState m_modelState{IdleState};
218+};
219+
220+Q_DECLARE_METATYPE(TopLevelSurfaceList*)
221+Q_DECLARE_METATYPE(QAbstractListModel*)
222+
223+#endif // TOPLEVELSURFACELIST_H
plugins/WindowManager/WindowManagerPlugin.cppnew file+28
@@ -0,0 +1,28 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "WindowManagerPlugin.h"
18+
19+#include "TopLevelSurfaceList.h"
20+
21+#include <QtQml>
22+
23+void WindowManagerPlugin::registerTypes(const char *uri)
24+{
25+ qmlRegisterType<TopLevelSurfaceList>(uri, 0, 1, "TopLevelSurfaceList");
26+
27+ qRegisterMetaType<QAbstractListModel*>("QAbstractListModel*");
28+}
tests/mocks/Unity/Application/SessionManager.h → plugins/WindowManager/WindowManagerPlugin.hrenamed+10−22
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -14,31 +14,19 @@
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
15 */
16
16
17−#ifndef SESSIONMANAGER_H
17+#ifndef WINDOWMANAGER_PLUGIN_H
18−#define SESSIONMANAGER_H
18+#define WINDOWMANAGER_PLUGIN_H
19
19
20−#include <QObject>
20+#include <QtQml/QQmlEngine>
21+#include <QtQml/QQmlExtensionPlugin>
21
22
22−class Session;
23+class WindowManagerPlugin : public QQmlExtensionPlugin
23−
24−class SessionManager : public QObject
25{
24{
26 Q_OBJECT
25 Q_OBJECT
27−public:
26+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
28− explicit SessionManager(QObject *parent = 0);
29−
30− static SessionManager *singleton();
31−
32− Q_INVOKABLE Session* createSession(const QString& name, const QUrl& screenshot);
33
27
34− void registerSession(Session *session);
28+public:
35−
29+ void registerTypes(const char *uri) override;
36−Q_SIGNALS:
37− void sessionStarting(Session *session);
38− void sessionStopping(Session *session);
39−
40−private:
41− static SessionManager *the_session_manager;
42};
30};
43
31
44−#endif // SESSIONMANAGER_H
32+#endif // WINDOWMANAGER_PLUGIN_H
plugins/WindowManager/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module WindowManager
2+plugin windowmanager-qml
qml/Components/KeymapSwitcher.qml+1−9
@@ -57,16 +57,8 @@ QtObject {
57 currentKeymapIndex = prevIndex;
57 currentKeymapIndex = prevIndex;
58 }
58 }
59
59
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−
67 property Binding surfaceKeymapBinding: Binding {
60 property Binding surfaceKeymapBinding: Binding {
68− target: root.surface
61+ target: MirFocusController.focusedSurface
69− when: root.surface != null && root.surface.live
70 property: "keymap"
62 property: "keymap"
71 value: root.currentKeymap
63 value: root.currentKeymap
72 }
64 }
qml/Greeter/Greeter.qml+5−4
@@ -64,7 +64,7 @@ Showable {
64 d.selectUser(d.currentIndex, true);
64 d.selectUser(d.currentIndex, true);
65 }
65 }
66
66
67− function notifyAppFocused(appId) {
67+ function notifyAppFocusRequested(appId) {
68 if (!active) {
68 if (!active) {
69 return;
69 return;
70 }
70 }
@@ -81,19 +81,20 @@ Showable {
81 }
81 }
82 }
82 }
83
83
84− function notifyAboutToFocusApp(appId) {
84+ // Notify that the user has explicitly requested the given app through unity8 GUI.
85+ function notifyUserRequestedApp(appId) {
85 if (!active) {
86 if (!active) {
86 return;
87 return;
87 }
88 }
88
89
89 // A hint that we're about to focus an app. This way we can look
90 // A hint that we're about to focus an app. This way we can look
90 // a little more responsive, rather than waiting for the above
91 // 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
92 // app, in order to show lockscreen instead of new app.
93 // app, in order to show lockscreen instead of new app.
93 d.startUnlock(false /* toTheRight */);
94 d.startUnlock(false /* toTheRight */);
94 }
95 }
95
96
96− // This is a just a glorified notifyAboutToFocusApp(), but it does one
97+ // This is a just a glorified notifyUserRequestedApp(), but it does one
97 // other thing: it hides any cover pages to the RIGHT, because the user
98 // other thing: it hides any cover pages to the RIGHT, because the user
98 // just came from a launcher drag starting on the left.
99 // just came from a launcher drag starting on the left.
99 // It also returns a boolean value, indicating whether there was a visual
100 // It also returns a boolean value, indicating whether there was a visual
qml/Launcher/LauncherPanel.qml+1
@@ -98,6 +98,7 @@ Rectangle {
98 AbstractButton {
98 AbstractButton {
99 id: dashItem
99 id: dashItem
100 anchors.fill: parent
100 anchors.fill: parent
101+ activeFocusOnPress: false
101 onClicked: root.showDashHome()
102 onClicked: root.showDashHome()
102 }
103 }
103 Rectangle {
104 Rectangle {
qml/Shell.qml+67−53
@@ -42,6 +42,7 @@ import Unity.Session 0.1
42import Unity.DashCommunicator 0.1
42import Unity.DashCommunicator 0.1
43import Unity.Indicators 0.1 as Indicators
43import Unity.Indicators 0.1 as Indicators
44import Cursor 1.0
44import Cursor 1.0
45+import WindowManager 0.1
45
46
46
47
47Item {
48Item {
@@ -96,12 +97,44 @@ Item {
96 }
97 }
97 }
98 }
98
99
99− // For autopilot consumption
100− readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
101−
102− // internal props from here onwards
103 readonly property var mainApp:
100 readonly property var mainApp:
104 applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainApp : null
101 applicationsDisplayLoader.item ? applicationsDisplayLoader.item.mainApp : null
102+ onMainAppChanged: {
103+ if (mainApp) {
104+ _onMainAppChanged(mainApp.appId);
105+ }
106+ }
107+ Connections {
108+ target: ApplicationManager
109+ onFocusRequested: {
110+ if (shell.mainApp && shell.mainApp.appId === appId) {
111+ _onMainAppChanged(appId);
112+ }
113+ }
114+ }
115+ function _onMainAppChanged(appId) {
116+ if (wizard.active && appId != "" && appId != "unity8-dash") {
117+ // If this happens on first boot, we may be in edge
118+ // tutorial or wizard while receiving a call. But a call
119+ // is more important than wizard so just bail out of those.
120+ tutorial.finish();
121+ wizard.hide();
122+ }
123+
124+ if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
125+ // If we are in the middle of a call, make dialer lockedApp and show it.
126+ // This can happen if user backs out of dialer back to greeter, then
127+ // launches dialer again.
128+ greeter.lockedApp = appId;
129+ }
130+ greeter.notifyAppFocusRequested(appId);
131+
132+ panel.indicators.hide();
133+ launcher.hide();
134+ }
135+
136+ // For autopilot consumption
137+ readonly property string focusedApplicationId: ApplicationManager.focusedApplicationId
105
138
106 // Disable everything while greeter is waiting, so that the user can't swipe
139 // Disable everything while greeter is waiting, so that the user can't swipe
107 // the greeter or launcher until we know whether the session is locked.
140 // the greeter or launcher until we know whether the session is locked.
@@ -139,9 +172,6 @@ Item {
139
172
140 Component.onCompleted: {
173 Component.onCompleted: {
141 theme.name = "Ubuntu.Components.Themes.SuruDark"
174 theme.name = "Ubuntu.Components.Themes.SuruDark"
142− if (ApplicationManager.count > 0) {
143− ApplicationManager.focusApplication(ApplicationManager.get(0).appId);
144− }
145 finishStartUpTimer.start();
175 finishStartUpTimer.start();
146 }
176 }
147
177
@@ -198,36 +228,10 @@ Item {
198 width: parent.width
228 width: parent.width
199 height: parent.height
229 height: parent.height
200
230
201− Connections {
231+ TopLevelSurfaceList {
202− target: ApplicationManager
232+ id: topLevelSurfaceList
203−
233+ objectName: "topLevelSurfaceList"
204− // This signal is also fired when we try to focus the current app
234+ applicationsModel: ApplicationManager
205− // again. We rely on this!
206− onFocusedApplicationIdChanged: {
207− var appId = ApplicationManager.focusedApplicationId;
208−
209− if (wizard.active && appId != "" && appId != "unity8-dash") {
210− // If this happens on first boot, we may be in edge
211− // tutorial or wizard while receiving a call. But a call
212− // is more important than wizard so just bail out of those.
213− tutorial.finish();
214− wizard.hide();
215− }
216−
217− if (appId === "dialer-app" && callManager.hasCalls && greeter.locked) {
218− // If we are in the middle of a call, make dialer lockedApp and show it.
219− // This can happen if user backs out of dialer back to greeter, then
220− // launches dialer again.
221− greeter.lockedApp = appId;
222− }
223− greeter.notifyAppFocused(appId);
224−
225− panel.indicators.hide();
226− }
227−
228− onApplicationAdded: {
229− launcher.hide();
230− }
231 }
235 }
232
236
233 Loader {
237 Loader {
@@ -258,6 +262,10 @@ Item {
258 return "Stages/DesktopStage.qml";
262 return "Stages/DesktopStage.qml";
259 }
263 }
260 }
264 }
265+ // TODO: Ensure the current stage is destroyed before the new one gets loaded.
266+ // Currently the new one will get loaded while the old is still hanging
267+ // around for a bit, which might lead to conflicts where both stages
268+ // change the model simultaneously.
261 onQmlComponentChanged: {
269 onQmlComponentChanged: {
262 if (item) item.stageAboutToBeUnloaded();
270 if (item) item.stageAboutToBeUnloaded();
263 source = qmlComponent;
271 source = qmlComponent;
@@ -270,6 +278,11 @@ Item {
270
278
271 onInteractiveChanged: { if (interactive) { focus = true; } }
279 onInteractiveChanged: { if (interactive) { focus = true; } }
272
280
281+ Binding {
282+ target: applicationsDisplayLoader.item
283+ property: "focus"
284+ value: true
285+ }
273 Binding {
286 Binding {
274 target: applicationsDisplayLoader.item
287 target: applicationsDisplayLoader.item
275 property: "objectName"
288 property: "objectName"
@@ -356,6 +369,16 @@ Item {
356 property: "leftMargin"
369 property: "leftMargin"
357 value: shell.usageScenario == "desktop" && !settings.autohideLauncher ? launcher.panelWidth: 0
370 value: shell.usageScenario == "desktop" && !settings.autohideLauncher ? launcher.panelWidth: 0
358 }
371 }
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+ }
359 }
382 }
360 }
383 }
361
384
@@ -370,16 +393,6 @@ Item {
370 z: notifications.useModal || panel.indicators.shown || wizard.active || tutorial.running ? overlay.z + 1 : overlay.z - 1
393 z: notifications.useModal || panel.indicators.shown || wizard.active || tutorial.running ? overlay.z + 1 : overlay.z - 1
371 }
394 }
372
395
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−
383 Loader {
396 Loader {
384 id: greeterLoader
397 id: greeterLoader
385 anchors.fill: parent
398 anchors.fill: parent
@@ -471,7 +484,7 @@ Item {
471 }
484 }
472
485
473 function showHome() {
486 function showHome() {
474− greeter.notifyAboutToFocusApp("unity8-dash");
487+ greeter.notifyUserRequestedApp("unity8-dash");
475
488
476 var animate = !lightDM.greeter.active && !stages.shown
489 var animate = !lightDM.greeter.active && !stages.shown
477 dash.setCurrentScope(0, animate, false)
490 dash.setCurrentScope(0, animate, false)
@@ -520,9 +533,10 @@ Item {
520 greeterShown: greeter.shown
533 greeterShown: greeter.shown
521 }
534 }
522
535
523− readonly property bool topmostApplicationIsFullscreen: mainApp && mainApp.fullscreen
536+ readonly property bool focusedSurfaceIsFullscreen: MirFocusController.focusedSurface
524−
537+ ? MirFocusController.focusedSurface.state === Mir.FullscreenState
538+ : false
525− fullscreenMode: (topmostApplicationIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
539+ fullscreenMode: (focusedSurfaceIsFullscreen && !lightDM.greeter.active && launcher.progress == 0)
526 || greeter.hasLockedApp
540 || greeter.hasLockedApp
527 locked: greeter && greeter.active
541 locked: greeter && greeter.active
528 }
542 }
@@ -555,7 +569,7 @@ Item {
555 }
569 }
556 }
570 }
557 onLauncherApplicationSelected: {
571 onLauncherApplicationSelected: {
558− greeter.notifyAboutToFocusApp(appId);
572+ greeter.notifyUserRequestedApp(appId);
559 shell.activateApplication(appId);
573 shell.activateApplication(appId);
560 }
574 }
561 onShownChanged: {
575 onShownChanged: {
@@ -719,7 +733,7 @@ Item {
719 onMouseMoved: { cursor.opacity = 1; }
733 onMouseMoved: { cursor.opacity = 1; }
720 }
734 }
721
735
722− // non-visual item
736+ // non-visual object
723 KeymapSwitcher {}
737 KeymapSwitcher {}
724
738
725 Rectangle {
739 Rectangle {
qml/Stages/AbstractStage.qml+9−2
@@ -18,12 +18,19 @@ import QtQuick 2.4
18import Ubuntu.Components 1.3
18import Ubuntu.Components 1.3
19import GSettings 1.0
19import GSettings 1.0
20
20
21−Rectangle {
21+FocusScope {
22 id: root
22 id: root
23
23
24+ property alias color: backRect.color
25+ Rectangle {
26+ id: backRect
24− color: "#060606"
27+ color: "#060606"
28+ anchors.fill: parent
29+ }
25
30
26 // Controls to be set from outside
31 // Controls to be set from outside
32+ property QtObject applicationManager
33+ property QtObject topLevelSurfaceList
27 property bool altTabPressed
34 property bool altTabPressed
28 property url background
35 property url background
29 property bool beingResized
36 property bool beingResized
qml/Stages/Animations/BaseSessionAnimation.qmldeleted−96
@@ -1,96 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−
19−/* This is the base case for surface animations, used when adding/removing * child surfaces.
20− * The class is meant to be overridden and changes/animations provided for state changes.
21− * NB. It is important to release the surface at the end of the "out" animation.
22− *
23− * Example - Simple fade in/out
24− *
25− * BaseSurfaceAnimation {
26− * outChanges: [ PropertyChanges { target: animation.surface; opacity: 0.0 } ]
27− * outAnimations: [
28− SequentialAnimation {
29− * NumberAnimation { target: animation.surface; property: "opacity"; duration: 300 }
30− * ScriptAction { script: { if (animation.parent.removing) animation.surface.release(); } }
31− * }
32− * ]
33− *
34− * inChanges: [ PropertyChanges { target: animation.surface; opacity: 1.0 } ]
35− * inAnimations: [ NumberAnimation { target: animation.surface; property: "opacity"; duration: 300 } ]
36− * }
37− */
38−Item {
39− id: base
40− property var container
41− objectName: "sessionAnimation"
42−
43− // changes applied when state changes to "from"
44− property list<QtObject> fromChanges
45− // transition animations when changing state to "from"
46− property list<QtObject> fromAnimations
47−
48− // changes applied when state changes to "to"
49− property list<QtObject> toChanges
50− // transition animations when changing state to "to"
51− property list<QtObject> toAnimations
52−
53− function start() {
54− // "prep" state forces toChanges without transition animations.
55− state = "prep"
56− state = "to";
57− }
58− function end() {
59− state = "from";
60− }
61−
62− signal completed()
63−
64− states: [
65− State {
66− name: "baseAnimation"
67− PropertyChanges { target: container; anchors.fill: undefined }
68− },
69−
70− State {
71− name: "prep"
72− extend: "baseAnimation"
73− changes: fromChanges
74− },
75− State {
76− name: "from"
77− extend: "prep"
78− },
79− State {
80− name: "in"
81− extend: "baseAnimation"
82− changes: toChanges
83− }
84− ]
85−
86− transitions: [
87− Transition {
88− to: "from"
89− animations: fromAnimations
90− },
91− Transition {
92− to: "to"
93− animations: toAnimations
94− }
95− ]
96−}
qml/Stages/Animations/SwipeFromBottomAnimation.qmldeleted−53
@@ -1,53 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import Ubuntu.Components 1.3
19−
20−BaseSessionAnimation {
21− id: animation
22−
23− fromChanges: [
24− AnchorChanges {
25− target: container;
26− anchors.top: container.parent.bottom
27− }
28− ]
29− fromAnimations: [
30− SequentialAnimation {
31− // clip so we don't go out of parent's bounds during spread
32− PropertyAction { target: container.parent; property: "clip"; value: true }
33− AnchorAnimation { easing: UbuntuAnimation.StandardEasing; duration: UbuntuAnimation.BriskDuration }
34− PropertyAction { target: container.parent; property: "clip"; value: false }
35− ScriptAction { script: { animation.completed(); } }
36− }
37− ]
38−
39− toChanges: [
40− AnchorChanges {
41− target: container;
42− anchors.top: container.parent.top
43− }
44− ]
45− toAnimations: [
46− SequentialAnimation {
47− // clip so we don't go out of parent's bounds during spread
48− PropertyAction { target: container.parent; property: "clip"; value: true }
49− AnchorAnimation { easing: UbuntuAnimation.StandardEasing; duration: UbuntuAnimation.BriskDuration }
50− PropertyAction { target: container.parent; property: "clip"; value: false }
51− }
52− ]
53−}
qml/Stages/ApplicationWindow.qml+110−68
@@ -20,35 +20,77 @@ import Unity.Application 0.1
20
20
21FocusScope {
21FocusScope {
22 id: root
22 id: root
23− implicitWidth: sessionContainer.implicitWidth
23+ implicitWidth: surfaceContainer.implicitWidth
24− implicitHeight: sessionContainer.implicitHeight
24+ implicitHeight: surfaceContainer.implicitHeight
25
25
26 // to be read from outside
26 // to be read from outside
27− property alias interactive: sessionContainer.interactive
27+ property alias interactive: surfaceContainer.interactive
28 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
28 property bool orientationChangesEnabled: d.supportsSurfaceResize ? d.surfaceOldEnoughToBeResized : true
29− readonly property string title: sessionContainer.surface && sessionContainer.surface.name !== "" ?
29+ readonly property string title: surface && surface.name !== "" ? surface.name : d.name
30− sessionContainer.surface.name : d.name
31
30
32 // overridable from outside
31 // 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+ }
34
41
35 // to be set from outside
42 // to be set from outside
43+ property QtObject surface
36 property QtObject application
44 property QtObject application
37 property int surfaceOrientationAngle
45 property int surfaceOrientationAngle
38− property alias resizeSurface: sessionContainer.resizeSurface
46+ property alias resizeSurface: surfaceContainer.resizeSurface
39 property int requestedWidth: -1
47 property int requestedWidth: -1
40 property int requestedHeight: -1
48 property int requestedHeight: -1
41
49
42− readonly property int minimumWidth: sessionContainer.surface ? sessionContainer.surface.minimumWidth : 0
50+ readonly property int minimumWidth: surface ? surface.minimumWidth : 0
43− readonly property int minimumHeight: sessionContainer.surface ? sessionContainer.surface.minimumHeight : 0
51+ readonly property int minimumHeight: surface ? surface.minimumHeight : 0
44− readonly property int maximumWidth: sessionContainer.surface ? sessionContainer.surface.maximumWidth : 0
52+ readonly property int maximumWidth: surface ? surface.maximumWidth : 0
45− readonly property int maximumHeight: sessionContainer.surface ? sessionContainer.surface.maximumHeight : 0
53+ readonly property int maximumHeight: surface ? surface.maximumHeight : 0
46− readonly property int widthIncrement: sessionContainer.surface ? sessionContainer.surface.widthIncrement : 0
54+ readonly property int widthIncrement: surface ? surface.widthIncrement : 0
47− readonly property int heightIncrement: sessionContainer.surface ? sessionContainer.surface.heightIncrement : 0
55+ readonly property int heightIncrement: surface ? surface.heightIncrement : 0
56+
57+ onSurfaceChanged: {
58+ // The order in which the instructions are executed here matters, to that the correct state
59+ // transitions in stateGroup take place.
60+ // More specifically, the moment surfaceContainer.surface gets updated relative to the
61+ // other instructions.
62+ if (surface) {
63+ surfaceContainer.surface = surface;
64+ d.liveSurface = surface.live;
65+ d.hadSurface = false;
66+ surfaceInitTimer.start();
67+ } else {
68+ if (d.surfaceInitialized) {
69+ d.hadSurface = true;
70+ }
71+ d.surfaceInitialized = false;
72+ surfaceContainer.surface = null;
73+ }
74+ }
48
75
49 QtObject {
76 QtObject {
50 id: d
77 id: d
51
78
79+ property bool liveSurface: false;
80+ property var con: Connections {
81+ target: root.surface
82+ onLiveChanged: d.liveSurface = root.surface.live
83+ }
84+ // using liveSurface instead of root.surface.live because with the latter
85+ // this expression is not reevaluated when root.surface changes
86+ readonly property bool needToTakeScreenshot: root.surface && d.surfaceInitialized && !d.liveSurface
87+ && applicationState !== ApplicationInfoInterface.Running
88+ onNeedToTakeScreenshotChanged: {
89+ if (needToTakeScreenshot && screenshotImage.status === Image.Null) {
90+ screenshotImage.take();
91+ }
92+ }
93+
52 // helpers so that we don't have to check for the existence of an application everywhere
94 // helpers so that we don't have to check for the existence of an application everywhere
53 // (in order to avoid breaking qml binding due to a javascript exception)
95 // (in order to avoid breaking qml binding due to a javascript exception)
54 readonly property string name: root.application ? root.application.name : ""
96 readonly property string name: root.application ? root.application.name : ""
@@ -62,17 +104,7 @@ FocusScope {
62 readonly property color splashColorFooter: root.application ? root.application.splashColorFooter : "#00000000"
104 readonly property color splashColorFooter: root.application ? root.application.splashColorFooter : "#00000000"
63
105
64 // Whether the Application had a surface before but lost it.
106 // Whether the Application had a surface before but lost it.
65− property bool hadSurface: sessionContainer.surfaceContainer.hadSurface
107+ property bool hadSurface: false
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− }
76
108
77 //FIXME - this is a hack to avoid the first few rendered frames as they
109 //FIXME - this is a hack to avoid the first few rendered frames as they
78 // might show the UI accommodating due to surface resizes on startup.
110 // might show the UI accommodating due to surface resizes on startup.
@@ -99,7 +131,9 @@ FocusScope {
99 Timer {
131 Timer {
100 id: surfaceInitTimer
132 id: surfaceInitTimer
101 interval: 100
133 interval: 100
134+ onTriggered: {
102− onTriggered: { if (sessionContainer.surface) {d.surfaceInitialized = true;} }
135+ if (root.surface && root.surface.live) {d.surfaceInitialized = true;}
136+ }
103 }
137 }
104
138
105 Timer {
139 Timer {
@@ -117,7 +151,7 @@ FocusScope {
117 function take() {
151 function take() {
118 // Save memory by using a half-resolution (thus quarter size) screenshot.
152 // Save memory by using a half-resolution (thus quarter size) screenshot.
119 // Do not make this a binding, we can only take the screenshot once!
153 // Do not make this a binding, we can only take the screenshot once!
120− sessionContainer.grabToImage(
154+ surfaceContainer.grabToImage(
121 function(result) {
155 function(result) {
122 screenshotImage.source = result.url;
156 screenshotImage.source = result.url;
123 },
157 },
@@ -144,46 +178,33 @@ FocusScope {
144 }
178 }
145 }
179 }
146
180
147− SessionContainer {
181+ SurfaceContainer {
148− id: sessionContainer
182+ id: surfaceContainer
149− // A fake application might not even have a session property.
150− session: application && application.session ? application.session : null
151−
152 requestedWidth: root.requestedWidth
183 requestedWidth: root.requestedWidth
153 requestedHeight: root.requestedHeight
184 requestedHeight: root.requestedHeight
154−
155 surfaceOrientationAngle: application && application.rotatesWindowContents ? root.surfaceOrientationAngle : 0
185 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−
165 focus: true
186 focus: true
166 }
187 }
167
188
168− // SessionContainer size drives ApplicationWindow size
189+ // SurfaceContainer size drives ApplicationWindow size
169 Binding {
190 Binding {
170 target: root; property: "width"
191 target: root; property: "width"
171− value: stateGroup.state === "surface" ? sessionContainer.width : root.requestedWidth
192+ value: stateGroup.state === "surface" ? surfaceContainer.width : root.requestedWidth
172 when: root.requestedWidth >= 0
193 when: root.requestedWidth >= 0
173 }
194 }
174 Binding {
195 Binding {
175 target: root; property: "height"
196 target: root; property: "height"
176− value: stateGroup.state === "surface" ? sessionContainer.height : root.requestedHeight
197+ value: stateGroup.state === "surface" ? surfaceContainer.height : root.requestedHeight
177 when: root.requestedHeight >= 0
198 when: root.requestedHeight >= 0
178 }
199 }
179
200
180− // ApplicationWindow size drives SessionContainer size
201+ // ApplicationWindow size drives SurfaceContainer size
181 Binding {
202 Binding {
182− target: sessionContainer; property: "width"; value: root.width
203+ target: surfaceContainer; property: "width"; value: root.width
183 when: root.requestedWidth < 0
204 when: root.requestedWidth < 0
184 }
205 }
185 Binding {
206 Binding {
186− target: sessionContainer; property: "height"; value: root.height
207+ target: surfaceContainer; property: "height"; value: root.height
187 when: root.requestedHeight < 0
208 when: root.requestedHeight < 0
188 }
209 }
189
210
@@ -194,32 +215,43 @@ FocusScope {
194 State {
215 State {
195 name: "void"
216 name: "void"
196 when:
217 when:
197− d.hadSurface && (!sessionContainer.surface || !d.surfaceInitialized)
218+ d.hadSurface && (!root.surface || !d.surfaceInitialized)
198 &&
219 &&
199 screenshotImage.status !== Image.Ready
220 screenshotImage.status !== Image.Ready
200 },
221 },
201 State {
222 State {
202 name: "splashScreen"
223 name: "splashScreen"
203 when:
224 when:
204− !d.hadSurface && (!sessionContainer.surface || !d.surfaceInitialized)
225+ !d.hadSurface && (!root.surface || !d.surfaceInitialized)
205 &&
226 &&
206 screenshotImage.status !== Image.Ready
227 screenshotImage.status !== Image.Ready
207 },
228 },
208 State {
229 State {
209 name: "surface"
230 name: "surface"
210 when:
231 when:
211− (sessionContainer.surface && d.surfaceInitialized)
232+ (root.surface && d.surfaceInitialized)
212 &&
233 &&
234+ (d.liveSurface ||
213− (d.applicationState !== ApplicationInfoInterface.Stopped
235+ (d.applicationState !== ApplicationInfoInterface.Running
214− || screenshotImage.status !== Image.Ready)
236+ && screenshotImage.status !== Image.Ready))
215 },
237 },
216 State {
238 State {
217 name: "screenshot"
239 name: "screenshot"
218 when:
240 when:
219 screenshotImage.status === Image.Ready
241 screenshotImage.status === Image.Ready
220 &&
242 &&
221− (d.applicationState === ApplicationInfoInterface.Stopped
243+ (d.applicationState !== ApplicationInfoInterface.Running
222− || !sessionContainer.surface || !d.surfaceInitialized)
244+ || !root.surface || !d.surfaceInitialized)
245+ },
246+ State {
247+ // This is a dead end. From here we expect the surface to be removed from the model
248+ // shortly after we stop referencing to it in our SurfaceContainer.
249+ name: "closed"
250+ when:
251+ // The surface died while the application is running. It must have been closed
252+ // by the shell or the application decided to destroy it by itself
253+ root.surface && d.surfaceInitialized && !d.liveSurface
254+ && d.applicationState === ApplicationInfoInterface.Running
223 }
255 }
224 ]
256 ]
225
257
@@ -227,17 +259,17 @@ FocusScope {
227 Transition {
259 Transition {
228 from: ""; to: "splashScreen"
260 from: ""; to: "splashScreen"
229 PropertyAction { target: splashLoader; property: "active"; value: true }
261 PropertyAction { target: splashLoader; property: "active"; value: true }
230− PropertyAction { target: sessionContainer.surfaceContainer
262+ PropertyAction { target: surfaceContainer
231 property: "visible"; value: false }
263 property: "visible"; value: false }
232 },
264 },
233 Transition {
265 Transition {
234 from: "splashScreen"; to: "surface"
266 from: "splashScreen"; to: "surface"
235 SequentialAnimation {
267 SequentialAnimation {
236− PropertyAction { target: sessionContainer.surfaceContainer
268+ PropertyAction { target: surfaceContainer
237 property: "opacity"; value: 0.0 }
269 property: "opacity"; value: 0.0 }
238− PropertyAction { target: sessionContainer.surfaceContainer
270+ PropertyAction { target: surfaceContainer
239 property: "visible"; value: true }
271 property: "visible"; value: true }
240− UbuntuNumberAnimation { target: sessionContainer.surfaceContainer; property: "opacity";
272+ UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
241 from: 0.0; to: 1.0
273 from: 0.0; to: 1.0
242 duration: UbuntuAnimation.BriskDuration }
274 duration: UbuntuAnimation.BriskDuration }
243 ScriptAction { script: {
275 ScriptAction { script: {
@@ -253,12 +285,12 @@ FocusScope {
253 surfaceIsOldTimer.stop();
285 surfaceIsOldTimer.stop();
254 d.surfaceOldEnoughToBeResized = false;
286 d.surfaceOldEnoughToBeResized = false;
255 splashLoader.active = true;
287 splashLoader.active = true;
256− sessionContainer.surfaceContainer.visible = true;
288+ surfaceContainer.visible = true;
257 } }
289 } }
258 UbuntuNumberAnimation { target: splashLoader; property: "opacity";
290 UbuntuNumberAnimation { target: splashLoader; property: "opacity";
259 from: 0.0; to: 1.0
291 from: 0.0; to: 1.0
260 duration: UbuntuAnimation.BriskDuration }
292 duration: UbuntuAnimation.BriskDuration }
261− PropertyAction { target: sessionContainer.surfaceContainer
293+ PropertyAction { target: surfaceContainer
262 property: "visible"; value: false }
294 property: "visible"; value: false }
263 }
295 }
264 },
296 },
@@ -274,15 +306,16 @@ FocusScope {
274 from: 0.0; to: 1.0
306 from: 0.0; to: 1.0
275 duration: UbuntuAnimation.BriskDuration }
307 duration: UbuntuAnimation.BriskDuration }
276 ScriptAction { script: {
308 ScriptAction { script: {
277− sessionContainer.surfaceContainer.visible = false;
309+ surfaceContainer.visible = false;
278− if (sessionContainer.session) { sessionContainer.session.release(); }
310+ surfaceContainer.surface = null;
311+ d.hadSurface = true;
279 } }
312 } }
280 }
313 }
281 },
314 },
282 Transition {
315 Transition {
283 from: "screenshot"; to: "surface"
316 from: "screenshot"; to: "surface"
284 SequentialAnimation {
317 SequentialAnimation {
285− PropertyAction { target: sessionContainer.surfaceContainer
318+ PropertyAction { target: surfaceContainer
286 property: "visible"; value: true }
319 property: "visible"; value: true }
287 UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
320 UbuntuNumberAnimation { target: screenshotImage; property: "opacity";
288 from: 1.0; to: 0.0
321 from: 1.0; to: 0.0
@@ -310,22 +343,31 @@ FocusScope {
310 ScriptAction { script: {
343 ScriptAction { script: {
311 surfaceIsOldTimer.stop();
344 surfaceIsOldTimer.stop();
312 d.surfaceOldEnoughToBeResized = false;
345 d.surfaceOldEnoughToBeResized = false;
313− sessionContainer.surfaceContainer.visible = false;
346+ surfaceContainer.visible = false;
314− if (sessionContainer.session) { sessionContainer.session.release(); }
315 } }
347 } }
316 },
348 },
317 Transition {
349 Transition {
318 from: "void"; to: "surface"
350 from: "void"; to: "surface"
319 SequentialAnimation {
351 SequentialAnimation {
320− PropertyAction { target: sessionContainer.surfaceContainer; property: "opacity"; value: 0.0 }
352+ PropertyAction { target: surfaceContainer; property: "opacity"; value: 0.0 }
321− PropertyAction { target: sessionContainer.surfaceContainer; property: "visible"; value: true }
353+ PropertyAction { target: surfaceContainer; property: "visible"; value: true }
322− UbuntuNumberAnimation { target: sessionContainer.surfaceContainer; property: "opacity";
354+ UbuntuNumberAnimation { target: surfaceContainer; property: "opacity";
323 from: 0.0; to: 1.0
355 from: 0.0; to: 1.0
324 duration: UbuntuAnimation.BriskDuration }
356 duration: UbuntuAnimation.BriskDuration }
325 ScriptAction { script: {
357 ScriptAction { script: {
326 surfaceIsOldTimer.start();
358 surfaceIsOldTimer.start();
327 } }
359 } }
328 }
360 }
361+ },
362+ Transition {
363+ to: "closed"
364+ SequentialAnimation {
365+ ScriptAction { script: {
366+ surfaceContainer.visible = false;
367+ surfaceContainer.surface = null;
368+ d.hadSurface = true;
369+ } }
370+ }
329 }
371 }
330 ]
372 ]
331 }
373 }
qml/Stages/DecoratedWindow.qml+5−5
@@ -26,10 +26,10 @@ FocusScope {
26 width: applicationWindow.width
26 width: applicationWindow.width
27 height: (decorationShown ? decoration.height : 0) + applicationWindow.height
27 height: (decorationShown ? decoration.height : 0) + applicationWindow.height
28
28
29− property alias window: applicationWindow
30 property alias application: applicationWindow.application
29 property alias application: applicationWindow.application
30+ property alias surface: applicationWindow.surface
31 property alias active: decoration.active
31 property alias active: decoration.active
32− property alias title: decoration.title
32+ readonly property alias title: applicationWindow.title
33 property alias fullscreen: applicationWindow.fullscreen
33 property alias fullscreen: applicationWindow.fullscreen
34
34
35 readonly property bool decorationShown: !fullscreen
35 readonly property bool decorationShown: !fullscreen
@@ -79,11 +79,11 @@ FocusScope {
79 WindowDecoration {
79 WindowDecoration {
80 id: decoration
80 id: decoration
81 target: root.parent
81 target: root.parent
82− objectName: application ? "appWindowDecoration_" + application.appId : "appWindowDecoration_null"
82+ objectName: "appWindowDecoration"
83 anchors { left: parent.left; top: parent.top; right: parent.right }
83 anchors { left: parent.left; top: parent.top; right: parent.right }
84 height: units.gu(3)
84 height: units.gu(3)
85 width: root.width
85 width: root.width
86− title: window.title
86+ title: applicationWindow.title
87 visible: root.decorationShown
87 visible: root.decorationShown
88
88
89 onClose: root.close();
89 onClose: root.close();
@@ -94,7 +94,7 @@ FocusScope {
94
94
95 ApplicationWindow {
95 ApplicationWindow {
96 id: applicationWindow
96 id: applicationWindow
97− objectName: application ? "appWindow_" + application.appId : "appWindow_null"
97+ objectName: "appWindow"
98 anchors.top: parent.top
98 anchors.top: parent.top
99 anchors.topMargin: decoration.height
99 anchors.topMargin: decoration.height
100 anchors.left: parent.left
100 anchors.left: parent.left
qml/Stages/DesktopSpread.qml+22−18
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
20import Ubuntu.Gestures 0.1
20import Ubuntu.Gestures 0.1
21import Unity.Application 0.1
21import Unity.Application 0.1
22import "../Components"
22import "../Components"
23+import Utils 0.1
23
24
24FocusScope {
25FocusScope {
25 id: root
26 id: root
@@ -69,12 +70,12 @@ FocusScope {
69 }
70 }
70
71
71 function selectNext(isAutoRepeat) {
72 function selectNext(isAutoRepeat) {
72− if (isAutoRepeat && spreadRepeater.highlightedIndex >= ApplicationManager.count -1) {
73+ if (isAutoRepeat && spreadRepeater.highlightedIndex >= topLevelSurfaceList.count -1) {
73 return; // AutoRepeat is not allowed to wrap around
74 return; // AutoRepeat is not allowed to wrap around
74 }
75 }
75
76
76− spreadRepeater.highlightedIndex = (spreadRepeater.highlightedIndex + 1) % ApplicationManager.count;
77+ spreadRepeater.highlightedIndex = (spreadRepeater.highlightedIndex + 1) % topLevelSurfaceList.count;
77− var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, spreadRepeater.highlightedIndex - 3));
78+ var newContentX = ((spreadFlickable.contentWidth) / (topLevelSurfaceList.count + 1)) * Math.max(0, Math.min(topLevelSurfaceList.count - 5, spreadRepeater.highlightedIndex - 3));
78 if (spreadFlickable.contentX < newContentX || spreadRepeater.highlightedIndex == 0) {
79 if (spreadFlickable.contentX < newContentX || spreadRepeater.highlightedIndex == 0) {
79 spreadFlickable.snapTo(newContentX)
80 spreadFlickable.snapTo(newContentX)
80 }
81 }
@@ -85,10 +86,10 @@ FocusScope {
85 return; // AutoRepeat is not allowed to wrap around
86 return; // AutoRepeat is not allowed to wrap around
86 }
87 }
87
88
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;
89 spreadRepeater.highlightedIndex = newIndex;
90 spreadRepeater.highlightedIndex = newIndex;
90− var newContentX = ((spreadFlickable.contentWidth) / (ApplicationManager.count + 1)) * Math.max(0, Math.min(ApplicationManager.count - 5, spreadRepeater.highlightedIndex - 1));
91+ var newContentX = ((spreadFlickable.contentWidth) / (topLevelSurfaceList.count + 1)) * Math.max(0, Math.min(topLevelSurfaceList.count - 5, spreadRepeater.highlightedIndex - 1));
91− if (spreadFlickable.contentX > newContentX || newIndex == ApplicationManager.count -1) {
92+ if (spreadFlickable.contentX > newContentX || newIndex == topLevelSurfaceList.count -1) {
92 spreadFlickable.snapTo(newContentX)
93 spreadFlickable.snapTo(newContentX)
93 }
94 }
94 }
95 }
@@ -98,8 +99,8 @@ FocusScope {
98 if (spreadContainer.visible) {
99 if (spreadContainer.visible) {
99 root.playFocusAnimation(spreadRepeater.highlightedIndex)
100 root.playFocusAnimation(spreadRepeater.highlightedIndex)
100 }
101 }
101− var application = ApplicationManager.get(spreadRepeater.highlightedIndex);
102+ var surface = topLevelSurfaceList.surfaceAt(spreadRepeater.highlightedIndex);
102− ApplicationManager.requestFocusApplication(application.appId);
103+ surface.requestFocus();
103 }
104 }
104 }
105 }
105
106
@@ -144,7 +145,7 @@ FocusScope {
144 Repeater {
145 Repeater {
145 id: spreadRepeater
146 id: spreadRepeater
146 objectName: "spreadRepeater"
147 objectName: "spreadRepeater"
147− model: ApplicationManager
148+ model: topLevelSurfaceList
148
149
149 property int highlightedIndex: -1
150 property int highlightedIndex: -1
150 property int closingIndex: -1
151 property int closingIndex: -1
@@ -182,7 +183,8 @@ FocusScope {
182 objectName: "clippedSpreadDelegate"
183 objectName: "clippedSpreadDelegate"
183 anchors.left: parent.left
184 anchors.left: parent.left
184 anchors.top: parent.top
185 anchors.top: parent.top
185− application: ApplicationManager.get(index)
186+ application: model.application
187+ surface: model.surface
186 width: spreadMaths.spreadHeight
188 width: spreadMaths.spreadHeight
187 height: spreadMaths.spreadHeight
189 height: spreadMaths.spreadHeight
188
190
@@ -216,7 +218,7 @@ FocusScope {
216 id: spreadMaths
218 id: spreadMaths
217 flickable: spreadFlickable
219 flickable: spreadFlickable
218 itemIndex: index
220 itemIndex: index
219− totalItems: Math.max(6, ApplicationManager.count)
221+ totalItems: Math.max(6, topLevelSurfaceList.count)
220 sceneHeight: root.height
222 sceneHeight: root.height
221 itemHeight: spreadDelegate.height
223 itemHeight: spreadDelegate.height
222 }
224 }
@@ -310,7 +312,7 @@ FocusScope {
310 Layout.preferredWidth: height * 8 / 7.6
312 Layout.preferredWidth: height * 8 / 7.6
311 image: Image {
313 image: Image {
312 anchors.fill: parent
314 anchors.fill: parent
313− source: model.icon
315+ source: model.application.icon
314 Rectangle {
316 Rectangle {
315 anchors.fill: parent
317 anchors.fill: parent
316 color: "black"
318 color: "black"
@@ -324,7 +326,9 @@ FocusScope {
324 Label {
326 Label {
325 Layout.fillWidth: true
327 Layout.fillWidth: true
326 Layout.preferredHeight: units.gu(6)
328 Layout.preferredHeight: units.gu(6)
327− text: model.name
329+ property string surfaceName: model.surface ? model.surface.name : ""
330+ property string applicationName: model.application ? model.application.name : ""
331+ text: surfaceName ? surfaceName : applicationName
328 wrapMode: Text.WordWrap
332 wrapMode: Text.WordWrap
329 elide: Text.ElideRight
333 elide: Text.ElideRight
330 maximumLineCount: 2
334 maximumLineCount: 2
@@ -354,7 +358,7 @@ FocusScope {
354 anchors.margins: -units.gu(2)
358 anchors.margins: -units.gu(2)
355 onClicked: {
359 onClicked: {
356 spreadRepeater.closingIndex = index;
360 spreadRepeater.closingIndex = index;
357− ApplicationManager.stopApplication(model.appId)
361+ model.surface.close();
358 }
362 }
359 }
363 }
360 }
364 }
@@ -421,7 +425,7 @@ FocusScope {
421 objectName: "spreadFlickable"
425 objectName: "spreadFlickable"
422 anchors.fill: parent
426 anchors.fill: parent
423 property int minContentWidth: 6 * Math.min(height / 4, width / 5)
427 property int minContentWidth: 6 * Math.min(height / 4, width / 5)
424− contentWidth: Math.max(6, ApplicationManager.count) * Math.min(height / 4, width / 5)
428+ contentWidth: Math.max(6, topLevelSurfaceList.count) * Math.min(height / 4, width / 5)
425 enabled: false
429 enabled: false
426
430
427 function snapTo(contentX) {
431 function snapTo(contentX) {
@@ -523,7 +527,7 @@ FocusScope {
523 Label {
527 Label {
524 id: currentSelectedLabel
528 id: currentSelectedLabel
525 anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
529 anchors { bottom: parent.bottom; bottomMargin: root.height * 0.625; horizontalCenter: parent.horizontalCenter }
526− text: spreadRepeater.highlightedIndex >= 0 ? ApplicationManager.get(spreadRepeater.highlightedIndex).name : ""
530+ text: spreadRepeater.highlightedIndex >= 0 ? topLevelSurfaceList.surfaceAt(spreadRepeater.highlightedIndex).name : ""
527 visible: false
531 visible: false
528 fontSize: "large"
532 fontSize: "large"
529 }
533 }
@@ -545,7 +549,7 @@ FocusScope {
545 from: "*"
549 from: "*"
546 to: "altTab"
550 to: "altTab"
547 SequentialAnimation {
551 SequentialAnimation {
548− PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(ApplicationManager.count - 1, 1) }
552+ PropertyAction { target: spreadRepeater; property: "highlightedIndex"; value: Math.min(topLevelSurfaceList.count - 1, 1) }
549 PauseAnimation { duration: spreadContainer.animateIn ? 0 : 140 }
553 PauseAnimation { duration: spreadContainer.animateIn ? 0 : 140 }
550 PropertyAction { target: workspaceSelector; property: "visible" }
554 PropertyAction { target: workspaceSelector; property: "visible" }
551 PropertyAction { target: spreadContainer; property: "visible" }
555 PropertyAction { target: spreadContainer; property: "visible" }
qml/Stages/DesktopSpreadDelegate.qml+4−3
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -25,12 +25,13 @@ Item {
25
25
26 property alias window: applicationWindow
26 property alias window: applicationWindow
27 property alias application: applicationWindow.application
27 property alias application: applicationWindow.application
28+ property alias surface: applicationWindow.surface
28
29
29 property bool highlightShown: false
30 property bool highlightShown: false
30 property real shadowOpacity: 1
31 property real shadowOpacity: 1
31
32
32− property int windowWidth: application && application.session && application.session.lastSurface ? application.session.lastSurface.size.width : 0
33+ property int windowWidth: surface ? surface.size.width : 0
33− property int windowHeight: application && application.session && application.session.lastSurface ? application.session.lastSurface.size.height : 0
34+ property int windowHeight: surface ? surface.size.height : 0
34
35
35 state: "normal"
36 state: "normal"
36 states: [
37 states: [
qml/Stages/DesktopStage.qml+159−98
@@ -39,45 +39,18 @@ AbstractStage {
39 // Used by TutorialRight
39 // Used by TutorialRight
40 property bool spreadShown: spread.state == "altTab"
40 property bool spreadShown: spread.state == "altTab"
41
41
42− mainApp: ApplicationManager.focusedApplicationId
42+ mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
43− ? ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
44− : null
45
43
46 // application windows never rotate independently
44 // application windows never rotate independently
47 mainAppWindowOrientationAngle: shellOrientationAngle
45 mainAppWindowOrientationAngle: shellOrientationAngle
48
46
49 orientationChangesEnabled: true
47 orientationChangesEnabled: true
50
48
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−
76 GlobalShortcut {
49 GlobalShortcut {
77 id: closeWindowShortcut
50 id: closeWindowShortcut
78 shortcut: Qt.AltModifier|Qt.Key_F4
51 shortcut: Qt.AltModifier|Qt.Key_F4
79− onTriggered: ApplicationManager.stopApplication(priv.focusedAppId)
52+ onTriggered: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
80− active: priv.focusedAppId !== ""
53+ active: priv.focusedAppDelegate !== null
81 }
54 }
82
55
83 GlobalShortcut {
56 GlobalShortcut {
@@ -121,40 +94,40 @@ AbstractStage {
121 active: priv.focusedAppDelegate !== null
94 active: priv.focusedAppDelegate !== null
122 }
95 }
123
96
97+ Connections {
98+ target: root.topLevelSurfaceList
99+ onCountChanged: {
100+ if (spread.state == "altTab") {
101+ spread.cancel();
102+ }
103+ }
104+ }
105+
124 QtObject {
106 QtObject {
125 id: priv
107 id: priv
108+ objectName: "DesktopStagePrivate"
126
109
127− readonly property string focusedAppId: ApplicationManager.focusedApplicationId
128− readonly property var focusedAppDelegate: {
110+ property var focusedAppDelegate: null
129− var index = indexOf(focusedAppId);
111+ onFocusedAppDelegateChanged: {
130− return index >= 0 && index < appRepeater.count ? appRepeater.itemAt(index) : null
112+ if (spread.state == "altTab") {
113+ spread.state = "";
114+ }
131 }
115 }
132− onFocusedAppDelegateChanged: updateForegroundMaximizedApp();
133
116
134− property int foregroundMaximizedAppZ: -1
135− property int foregroundMaximizedAppIndex: -1 // for stuff like drop shadow and focusing maximized app by clicking panel
117+ property var foregroundMaximizedAppDelegate: null // for stuff like drop shadow and focusing maximized app by clicking panel
136
118
137 function updateForegroundMaximizedApp() {
119 function updateForegroundMaximizedApp() {
138− var tmp = -1;
120+ var found = false;
139− var tmpAppId = -1;
140− for (var i = appRepeater.count - 1; i >= 0; i--) {
121+ for (var i = 0; i < appRepeater.count && !found; i++) {
141 var item = appRepeater.itemAt(i);
122 var item = appRepeater.itemAt(i);
142 if (item && item.visuallyMaximized) {
123 if (item && item.visuallyMaximized) {
143− tmpAppId = i;
124+ foregroundMaximizedAppDelegate = item;
144− tmp = Math.max(tmp, item.normalZ);
125+ found = true;
145 }
126 }
146 }
127 }
147− foregroundMaximizedAppZ = tmp;
128+ if (!found) {
148− foregroundMaximizedAppIndex = tmpAppId;
129+ foregroundMaximizedAppDelegate = null;
149− }
150−
151− function indexOf(appId) {
152− for (var i = 0; i < ApplicationManager.count; i++) {
153− if (ApplicationManager.get(i).appId == appId) {
154− return i;
155− }
156 }
130 }
157− return -1;
158 }
131 }
159
132
160 function minimizeAllWindows() {
133 function minimizeAllWindows() {
@@ -164,16 +137,13 @@ AbstractStage {
164 appDelegate.minimize();
137 appDelegate.minimize();
165 }
138 }
166 }
139 }
167−
168− ApplicationManager.unfocusCurrentApplication(); // no app should have focus at this point
169 }
140 }
170
141
171 function focusNext() {
142 function focusNext() {
172− ApplicationManager.unfocusCurrentApplication();
173 for (var i = 0; i < appRepeater.count; i++) {
143 for (var i = 0; i < appRepeater.count; i++) {
174 var appDelegate = appRepeater.itemAt(i);
144 var appDelegate = appRepeater.itemAt(i);
175 if (appDelegate && !appDelegate.minimized) {
145 if (appDelegate && !appDelegate.minimized) {
176− ApplicationManager.focusApplication(appDelegate.appId);
146+ appDelegate.focus = true;
177 return;
147 return;
178 }
148 }
179 }
149 }
@@ -182,15 +152,14 @@ AbstractStage {
182
152
183 Connections {
153 Connections {
184 target: PanelState
154 target: PanelState
185− onClose: {
155+ onClose: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.close(); } }
186− ApplicationManager.stopApplication(ApplicationManager.focusedApplicationId)
156+ onMinimize: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.minimize(); } }
157+ onMaximize: { if (priv.focusedAppDelegate) { priv.focusedAppDelegate.restoreFromMaximized(); } }
158+ onFocusMaximizedApp: {
159+ if (priv.foregroundMaximizedAppDelegate) {
160+ priv.foregroundMaximizedAppDelegate.focus = true;
161+ }
187 }
162 }
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− }
194 }
163 }
195
164
196 Binding {
165 Binding {
@@ -218,7 +187,7 @@ AbstractStage {
218 Binding {
187 Binding {
219 target: PanelState
188 target: PanelState
220 property: "dropShadow"
189 property: "dropShadow"
221− value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppIndex !== -1
190+ value: priv.focusedAppDelegate && !priv.focusedAppDelegate.maximized && priv.foregroundMaximizedAppDelegate !== null
222 }
191 }
223
192
224 Component.onDestruction: {
193 Component.onDestruction: {
@@ -227,6 +196,27 @@ AbstractStage {
227 PanelState.dropShadow = false;
196 PanelState.dropShadow = false;
228 }
197 }
229
198
199+ Instantiator {
200+ model: root.applicationManager
201+ delegate: Binding {
202+ target: model.application
203+ property: "requestedState"
204+
205+ // TODO: figure out some lifecycle policy, like suspending minimized apps
206+ // if running on a tablet or something.
207+ // TODO: If the device has a dozen suspended apps because it was running
208+ // in staged mode, when it switches to Windowed mode it will suddenly
209+ // resume all those apps at once. We might want to avoid that.
210+ value: ApplicationInfoInterface.RequestedRunning // Always running for now
211+ }
212+ }
213+
214+ Binding {
215+ target: MirFocusController
216+ property: "focusedSurface"
217+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
218+ when: !appRepeater.startingUp && root.parent
219+ }
230
220
231 FocusScope {
221 FocusScope {
232 id: appContainer
222 id: appContainer
@@ -242,20 +232,25 @@ AbstractStage {
242 fillMode: Image.PreserveAspectCrop
232 fillMode: Image.PreserveAspectCrop
243 }
233 }
244
234
245− Repeater {
235+ TopLevelSurfaceRepeater {
246 id: appRepeater
236 id: appRepeater
247− model: ApplicationManager
237+ model: topLevelSurfaceList
248 objectName: "appRepeater"
238 objectName: "appRepeater"
249
239
250 delegate: FocusScope {
240 delegate: FocusScope {
251 id: appDelegate
241 id: appDelegate
252− objectName: "appDelegate_" + appId
242+ objectName: "appDelegate_" + model.id
253 // z might be overriden in some cases by effects, but we need z ordering
243 // z might be overriden in some cases by effects, but we need z ordering
254 // to calculate occlusion detection
244 // to calculate occlusion detection
255− property int normalZ: ApplicationManager.count - index
245+ property int normalZ: topLevelSurfaceList.count - index
246+ onNormalZChanged: {
247+ if (visuallyMaximized) {
248+ priv.updateForegroundMaximizedApp();
249+ }
250+ }
256 z: normalZ
251 z: normalZ
257 y: PanelState.panelHeight
252 y: PanelState.panelHeight
258− focus: appId === priv.focusedAppId
253+
259 width: decoratedWindow.width
254 width: decoratedWindow.width
260 height: decoratedWindow.height
255 height: decoratedWindow.height
261 property int requestedWidth: -1
256 property int requestedWidth: -1
@@ -280,36 +275,99 @@ AbstractStage {
280 readonly property alias minimized: appDelegatePrivate.minimized
275 readonly property alias minimized: appDelegatePrivate.minimized
281 readonly property alias fullscreen: decoratedWindow.fullscreen
276 readonly property alias fullscreen: decoratedWindow.fullscreen
282
277
283− readonly property string appId: model.appId
278+ readonly property var application: model.application
284 property bool animationsEnabled: true
279 property bool animationsEnabled: true
285 property alias title: decoratedWindow.title
280 property alias title: decoratedWindow.title
286− readonly property string appName: model.name
281+ readonly property string appName: model.application ? model.application.name : ""
287 property bool visuallyMaximized: false
282 property bool visuallyMaximized: false
288 property bool visuallyMinimized: false
283 property bool visuallyMinimized: false
289
284
285+ readonly property var surface: model.surface
286+
287+ function claimFocus() {
288+ if (spread.state == "altTab") {
289+ spread.cancel();
290+ }
291+ appDelegate.restore();
292+ }
293+ Connections {
294+ target: model.surface
295+ onFocusRequested: claimFocus();
296+ }
297+ Connections {
298+ target: model.application
299+ onFocusRequested: {
300+ if (!model.surface) {
301+ // when an app has no surfaces, we assume there's only one entry representing it:
302+ // this delegate.
303+ claimFocus();
304+ } else {
305+ // if the application has surfaces, focus request should be at surface-level.
306+ }
307+ }
308+ }
309+
290 onFocusChanged: {
310 onFocusChanged: {
291− if (focus && ApplicationManager.focusedApplicationId !== appId) {
311+ if (appRepeater.startingUp)
292− ApplicationManager.focusApplication(appId);
312+ return;
313+
314+ if (focus) {
315+ priv.focusedAppDelegate = appDelegate;
316+
317+ // If we're orphan (!parent) it means this stage is no longer the current one
318+ // and will be deleted shortly. So we should no longer have a say over the model
319+ if (root.parent) {
320+ topLevelSurfaceList.raiseId(model.id);
321+ }
322+ } else if (!focus && priv.focusedAppDelegate === appDelegate) {
323+ priv.focusedAppDelegate = null;
324+ // FIXME: No idea why the Binding{} doens't update when focusedAppDelegate turns null
325+ MirFocusController.focusedSurface = null;
326+ }
327+ }
328+ Component.onCompleted: {
329+ // NB: We're differentiating if this delegate was created in response to a new entry in the model
330+ // or if the Repeater is just populating itself with delegates to match the model it received.
331+ if (!appRepeater.startingUp) {
332+ // a top level window is always the focused one when it first appears, unfocusing
333+ // any preexisting one
334+ focus = true;
335+ }
336+ }
337+ Component.onDestruction: {
338+ if (!root.parent) {
339+ // This stage is about to be destroyed. Don't mess up with the model at this point
340+ return;
341+ }
342+
343+ if (visuallyMaximized) {
344+ priv.updateForegroundMaximizedApp();
345+ }
346+
347+ if (focus) {
348+ // focus some other window
349+ for (var i = 0; i < appRepeater.count; i++) {
350+ var appDelegate = appRepeater.itemAt(i);
351+ if (appDelegate && !appDelegate.minimized && i != index) {
352+ appDelegate.focus = true;
353+ return;
354+ }
355+ }
293 }
356 }
294 }
357 }
295
358
296 onVisuallyMaximizedChanged: priv.updateForegroundMaximizedApp()
359 onVisuallyMaximizedChanged: priv.updateForegroundMaximizedApp()
297
360
361+ visible: (
298− visible: !visuallyMinimized &&
362+ !visuallyMinimized
299− !greeter.fullyShown &&
363+ && !greeter.fullyShown
300− (priv.foregroundMaximizedAppZ === -1 || priv.foregroundMaximizedAppZ <= z) ||
364+ && (priv.foregroundMaximizedAppDelegate === null || priv.foregroundMaximizedAppDelegate.normalZ <= z)
365+ )
301− decoratedWindow.fullscreen ||
366+ || decoratedWindow.fullscreen
302− (spread.state == "altTab" && index === spread.highlightedIndex)
367+ || (spread.state == "altTab" && index === spread.highlightedIndex)
303
368
304− Binding {
369+ function close() {
305− target: ApplicationManager.get(index)
370+ model.surface.close();
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
313 }
371 }
314
372
315 function maximize(animated) {
373 function maximize(animated) {
@@ -351,7 +409,8 @@ AbstractStage {
351 maximizeLeft();
409 maximizeLeft();
352 else if (maximizedRight)
410 else if (maximizedRight)
353 maximizeRight();
411 maximizeRight();
354− ApplicationManager.focusApplication(appId);
412+
413+ focus = true;
355 }
414 }
356
415
357 function playFocusAnimation() {
416 function playFocusAnimation() {
@@ -369,7 +428,7 @@ AbstractStage {
369
428
370 states: [
429 states: [
371 State {
430 State {
372− name: "fullscreen"; when: decoratedWindow.fullscreen
431+ name: "fullscreen"; when: decoratedWindow.fullscreen && !appDelegate.minimized
373 PropertyChanges {
432 PropertyChanges {
374 target: appDelegate;
433 target: appDelegate;
375 x: 0;
434 x: 0;
@@ -462,6 +521,7 @@ AbstractStage {
462 ScriptAction {
521 ScriptAction {
463 script: {
522 script: {
464 if (appDelegate.minimized) {
523 if (appDelegate.minimized) {
524+ appDelegate.focus = false;
465 priv.focusNext();
525 priv.focusNext();
466 }
526 }
467 }
527 }
@@ -486,7 +546,7 @@ AbstractStage {
486 id: previewBinding
546 id: previewBinding
487 target: appDelegate
547 target: appDelegate
488 property: "z"
548 property: "z"
489− value: ApplicationManager.count + 1
549+ value: topLevelSurfaceList.count + 1
490 when: index == spread.highlightedIndex && spread.ready
550 when: index == spread.highlightedIndex && spread.ready
491 }
551 }
492
552
@@ -497,12 +557,12 @@ AbstractStage {
497 minWidth: units.gu(10)
557 minWidth: units.gu(10)
498 minHeight: units.gu(10)
558 minHeight: units.gu(10)
499 borderThickness: units.gu(2)
559 borderThickness: units.gu(2)
500− windowId: model.appId // FIXME: Change this to point to windowId once we have such a thing
560+ windowId: model.application.appId // FIXME: Change this to point to windowId once we have such a thing
501 screenWidth: appContainer.width
561 screenWidth: appContainer.width
502 screenHeight: appContainer.height
562 screenHeight: appContainer.height
503 leftMargin: root.leftMargin
563 leftMargin: root.leftMargin
504
564
505− onPressed: { ApplicationManager.focusApplication(model.appId) }
565+ onPressed: { appDelegate.focus = true; }
506
566
507 Component.onCompleted: {
567 Component.onCompleted: {
508 loadWindowState();
568 loadWindowState();
@@ -529,24 +589,25 @@ AbstractStage {
529 objectName: "decoratedWindow"
589 objectName: "decoratedWindow"
530 anchors.left: appDelegate.left
590 anchors.left: appDelegate.left
531 anchors.top: appDelegate.top
591 anchors.top: appDelegate.top
532− application: ApplicationManager.get(index)
592+ application: model.application
533− active: ApplicationManager.focusedApplicationId === model.appId
593+ surface: model.surface
594+ active: appDelegate.focus
534 focus: true
595 focus: true
535
596
536 requestedWidth: appDelegate.requestedWidth
597 requestedWidth: appDelegate.requestedWidth
537 requestedHeight: appDelegate.requestedHeight
598 requestedHeight: appDelegate.requestedHeight
538
599
539− onClose: ApplicationManager.stopApplication(model.appId)
600+ onClose: { appDelegate.close(); }
540 onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
601 onMaximize: appDelegate.maximized || appDelegate.maximizedLeft || appDelegate.maximizedRight
541 ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
602 ? appDelegate.restoreFromMaximized() : appDelegate.maximize()
542 onMinimize: appDelegate.minimize()
603 onMinimize: appDelegate.minimize()
543− onDecorationPressed: { ApplicationManager.focusApplication(model.appId) }
604+ onDecorationPressed: { appDelegate.focus = true; }
544 }
605 }
545
606
546 WindowedFullscreenPolicy {
607 WindowedFullscreenPolicy {
547 id: fullscreenPolicy
608 id: fullscreenPolicy
548 active: true
609 active: true
549− application: decoratedWindow.application
610+ surface: model.surface
550 }
611 }
551 }
612 }
552 }
613 }
qml/Stages/PhoneStage.qml+152−89
@@ -26,7 +26,6 @@ import "../Components"
26AbstractStage {
26AbstractStage {
27 id: root
27 id: root
28
28
29− property QtObject applicationManager: ApplicationManager
30 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
29 property bool focusFirstApp: true // If false, focused app will appear on right edge like other apps
31 property bool altTabEnabled: true
30 property bool altTabEnabled: true
32 property real startScale: 1.1
31 property real startScale: 1.1
@@ -107,9 +106,7 @@ AbstractStage {
107 }
106 }
108 }
107 }
109
108
110− mainApp: applicationManager.focusedApplicationId
109+ mainApp: priv.focusedAppDelegate ? priv.focusedAppDelegate.application : null
111− ? applicationManager.findApplication(applicationManager.focusedApplicationId)
112− : null
113
110
114 orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
111 orientationChangesEnabled: priv.focusedAppOrientationChangesEnabled
115 && !priv.focusedAppDelegateIsDislocated
112 && !priv.focusedAppDelegateIsDislocated
@@ -158,72 +155,27 @@ AbstractStage {
158 onTriggered: { root.beingResized = false; }
155 onTriggered: { root.beingResized = false; }
159 }
156 }
160
157
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−
200 QtObject {
158 QtObject {
201 id: priv
159 id: priv
202
160
203− property string focusedAppId: root.applicationManager.focusedApplicationId
204 property bool focusedAppOrientationChangesEnabled: false
161 property bool focusedAppOrientationChangesEnabled: false
205 readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
162 readonly property int firstSpreadIndex: root.focusFirstApp ? 1 : 0
206− readonly property var focusedAppDelegate: {
163+ property var focusedAppDelegate
207− var index = indexOf(focusedAppId);
164+ // NB! This may differ from applicationManager.focusedApplicationId if focusedAppDelegate
208− return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
165+ // contains a screenshot instead of a surface.
209− }
166+ property string focusedAppId: focusedAppDelegate ? focusedAppDelegate.application.appId : ""
210
167
211 property real oldInverseProgress: 0
168 property real oldInverseProgress: 0
212 property bool animateX: false
169 property bool animateX: false
213 property int highlightIndex: 0
170 property int highlightIndex: 0
214
171
215− onFocusedAppDelegateChanged: {
216− if (focusedAppDelegate) {
217− focusedAppDelegate.focus = true;
218− }
219− }
220−
221− property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
172+ property bool focusedAppDelegateIsDislocated: focusedAppDelegate ?
222 (focusedAppDelegate.x !== 0 || focusedAppDelegate.xBehavior.running)
173 (focusedAppDelegate.x !== 0 || focusedAppDelegate.xBehavior.running)
174+ : false
223
175
224 function indexOf(appId) {
176 function indexOf(appId) {
225− for (var i = 0; i < root.applicationManager.count; i++) {
177+ for (var i = 0; i < spreadRepeater.count; i++) {
226− if (root.applicationManager.get(i).appId == appId) {
178+ if (spreadRepeater.itemAt(i).application.appId == appId) {
227 return i;
179 return i;
228 }
180 }
229 }
181 }
@@ -237,8 +189,8 @@ AbstractStage {
237 function reset() {
189 function reset() {
238 // The app that's about to go to foreground has to be focused, otherwise
190 // The app that's about to go to foreground has to be focused, otherwise
239 // it would leave us in an inconsistent state.
191 // it would leave us in an inconsistent state.
240− if (!root.applicationManager.focusedApplicationId && root.applicationManager.count > 0) {
192+ if (!MirFocusController.focusedSurface && spreadRepeater.count > 0) {
241− root.applicationManager.focusApplication(root.applicationManager.get(0).appId);
193+ spreadRepeater.itemAt(0).focus = true;
242 }
194 }
243
195
244 spreadView.selectedIndex = -1;
196 spreadView.selectedIndex = -1;
@@ -258,6 +210,36 @@ AbstractStage {
258 }
210 }
259 }
211 }
260
212
213+ Instantiator {
214+ model: root.applicationManager
215+ delegate: QtObject {
216+ property var stateBinding: Binding {
217+ readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
218+ target: model.application
219+ property: "requestedState"
220+ value: (isDash && root.keepDashRunning)
221+ || (!root.suspended && model.application && priv.focusedAppId === model.application.appId)
222+ ? ApplicationInfoInterface.RequestedRunning
223+ : ApplicationInfoInterface.RequestedSuspended
224+ }
225+
226+ property var lifecycleBinding: Binding {
227+ target: model.application
228+ property: "exemptFromLifecycle"
229+ value: model.application
230+ ? (!model.application.isTouchApp || isExemptFromLifecycle(model.application.appId))
231+ : false
232+ }
233+ }
234+ }
235+
236+ Binding {
237+ target: MirFocusController
238+ property: "focusedSurface"
239+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
240+ when: root.parent && !spreadRepeater.startingUp
241+ }
242+
261 Flickable {
243 Flickable {
262 id: spreadView
244 id: spreadView
263 objectName: "spreadView"
245 objectName: "spreadView"
@@ -308,13 +290,38 @@ AbstractStage {
308 // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
290 // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
309 // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
291 // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
310 // the Flickable.boundsBehavior upon release).
292 // the Flickable.boundsBehavior upon release).
293+ onContentXChanged: {
294+ if (!undoContentXReset()) {
311− onContentXChanged: { forceItToRemainStillIfBeingResized(); }
295+ forceItToRemainStillIfBeingResized();
296+ }
297+ }
312 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
298 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
313 function forceItToRemainStillIfBeingResized() {
299 function forceItToRemainStillIfBeingResized() {
314 if (root.beingResized && contentX != -spreadView.shift) {
300 if (root.beingResized && contentX != -spreadView.shift) {
315 contentX = -spreadView.shift;
301 contentX = -spreadView.shift;
316 }
302 }
317 }
303 }
304+ function undoContentXReset() {
305+ if (contentWidth <= 0) {
306+ contentWidthOnLastContentXChange = contentWidth;
307+ lastContentX = contentX;
308+ return false;
309+ }
310+
311+ if (contentWidth !== contentWidthOnLastContentXChange
312+ && lastContentX === -shift && contentX === 0) {
313+ // Flickable is resetting contentX because contentWidth has changed. Undo it.
314+ contentX = -shift;
315+ return true;
316+ }
317+
318+ contentWidthOnLastContentXChange = contentWidth;
319+ lastContentX = contentX;
320+ return false;
321+ }
322+ property real contentWidthOnLastContentXChange: -1
323+ property real lastContentX: 0
324+ // </FIXME-contentX>
318
325
319 Behavior on contentX {
326 Behavior on contentX {
320 enabled: root.altTabPressed
327 enabled: root.altTabPressed
@@ -377,7 +384,7 @@ AbstractStage {
377 snapAnimation.start();
384 snapAnimation.start();
378 return;
385 return;
379 }
386 }
380− if (root.applicationManager.count <= index) {
387+ if (topLevelSurfaceList.count <= index) {
381 // In case we're trying to snap to some non existing app, lets snap back to the first one
388 // In case we're trying to snap to some non existing app, lets snap back to the first one
382 index = 0;
389 index = 0;
383 }
390 }
@@ -414,7 +421,8 @@ AbstractStage {
414 ScriptAction {
421 ScriptAction {
415 script: {
422 script: {
416 if (spreadView.selectedIndex >= 0) {
423 if (spreadView.selectedIndex >= 0) {
417− root.applicationManager.focusApplication(root.applicationManager.get(spreadView.selectedIndex).appId);
424+ var delegate = spreadRepeater.itemAt(spreadView.selectedIndex)
425+ delegate.focus = true;
418
426
419 spreadView.selectedIndex = -1;
427 spreadView.selectedIndex = -1;
420 spreadView.phase = 0;
428 spreadView.phase = 0;
@@ -429,7 +437,7 @@ AbstractStage {
429 // This width controls how much the spread can be flicked left/right. It's composed of:
437 // This width controls how much the spread can be flicked left/right. It's composed of:
430 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
438 // tileDistance * app count (with a minimum of 3 apps, in order to also allow moving 1 and 2 apps a bit)
431 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
439 // + some constant value (still scales with the screen width) which looks good and somewhat fills the screen
432− width: Math.max(3, root.applicationManager.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
440+ width: Math.max(3, topLevelSurfaceList.count) * spreadView.tileDistance + (spreadView.width - spreadView.tileDistance) * 1.5
433 height: parent.height
441 height: parent.height
434 Behavior on width {
442 Behavior on width {
435 enabled: spreadView.closingIndex >= 0
443 enabled: spreadView.closingIndex >= 0
@@ -449,13 +457,30 @@ AbstractStage {
449 }
457 }
450 }
458 }
451
459
452− Repeater {
460+ TopLevelSurfaceRepeater {
453 id: spreadRepeater
461 id: spreadRepeater
454 objectName: "spreadRepeater"
462 objectName: "spreadRepeater"
455− model: root.applicationManager
463+ model: topLevelSurfaceList
464+
465+ onItemRemoved: {
466+ // Unless we're closing the app ourselves in the spread,
467+ // lets make sure the spread doesn't mess up by the changing app list.
468+ if (spreadView.closingIndex == -1) {
469+ spreadView.phase = 0;
470+ spreadView.contentX = -spreadView.shift;
471+ focusTopMostApp();
472+ }
473+ }
474+ function focusTopMostApp() {
475+ if (spreadRepeater.count > 0) {
476+ var topmostDelegate = spreadRepeater.itemAt(0);
477+ topmostDelegate.focus = true;
478+ }
479+ }
480+
456 delegate: TransformedSpreadDelegate {
481 delegate: TransformedSpreadDelegate {
457 id: appDelegate
482 id: appDelegate
458− objectName: "appDelegate" + index
483+ objectName: "spreadDelegate_" + model.id
459 startAngle: 45
484 startAngle: 45
460 endAngle: 5
485 endAngle: 5
461 startScale: root.startScale
486 startScale: root.startScale
@@ -467,28 +492,61 @@ AbstractStage {
467 selected: spreadView.selectedIndex == index
492 selected: spreadView.selectedIndex == index
468 otherSelected: spreadView.selectedIndex >= 0 && !selected
493 otherSelected: spreadView.selectedIndex >= 0 && !selected
469 interactive: !spreadView.interactive && spreadView.phase === 0
494 interactive: !spreadView.interactive && spreadView.phase === 0
470− && priv.fullyShowingFocusedApp && root.interactive && isFocused
495+ && priv.fullyShowingFocusedApp && root.interactive && focus
471 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
496 swipeToCloseEnabled: spreadView.interactive && root.interactive && !snapAnimation.running
472 maximizedAppTopMargin: root.maximizedAppTopMargin
497 maximizedAppTopMargin: root.maximizedAppTopMargin
473 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
498 dropShadow: spreadView.active || priv.focusedAppDelegateIsDislocated
474 focusFirstApp: root.focusFirstApp
499 focusFirstApp: root.focusFirstApp
475 highlightShown: root.altTabPressed && index === priv.highlightIndex
500 highlightShown: root.altTabPressed && index === priv.highlightIndex
476
501
477− readonly property bool isDash: model.appId == "unity8-dash"
502+ readonly property bool isDash: model.application.appId == "unity8-dash"
478−
503+
479− Binding {
504+ Component.onCompleted: {
480− target: appDelegate.application
505+ // NB: We're differentiating if this delegate was created in response to a new entry in the model
481− property: "exemptFromLifecycle"
506+ // or if the Repeater is just populating itself with delegates to match the model it received.
482− value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
507+ if (!spreadRepeater.startingUp) {
508+ // a top level window is always the focused one when it first appears, unfocusing
509+ // any preexisting one
510+ //
511+ // new items are appended and must be manually brought to front.
512+ // that's how it *must* be in order to get the animation for new
513+ // surfaces working
514+ claimFocus();
515+ }
483 }
516 }
484
517
485− Binding {
518+ onFocusChanged: {
519+ if (focus && !spreadRepeater.startingUp) {
520+ priv.focusedAppDelegate = appDelegate;
521+ // If we're orphan (!parent) it means this stage is no longer the current one
522+ // and will be deleted shortly. So we should no longer have a say over the model
523+ if (root.parent) {
524+ topLevelSurfaceList.raiseId(model.id);
525+ }
526+ }
527+ }
528+ function claimFocus() {
529+ if (spreadView.phase > 0) {
530+ spreadView.snapTo(model.index);
531+ } else {
486− target: appDelegate.application
532+ appDelegate.focus = true;
487− property: "requestedState"
533+ }
488− value: (isDash && root.keepDashRunning)
534+ }
489− || (!root.suspended && appDelegate.focus)
535+ Connections {
490− ? ApplicationInfoInterface.RequestedRunning
536+ target: model.surface
491− : ApplicationInfoInterface.RequestedSuspended
537+ onFocusRequested: claimFocus()
538+ }
539+ Connections {
540+ target: model.application
541+ onFocusRequested: {
542+ if (!model.surface) {
543+ // when an app has no surfaces, we assume there's only one entry representing it:
544+ // this delegate.
545+ claimFocus();
546+ } else {
547+ // if the application has surfaces, focus request should be at surface-level.
548+ }
549+ }
492 }
550 }
493
551
494 z: isDash && !spreadView.active ? -1 : behavioredIndex
552 z: isDash && !spreadView.active ? -1 : behavioredIndex
@@ -509,7 +567,8 @@ AbstractStage {
509 return spreadView.width + spreadIndex * spreadView.tileDistance;
567 return spreadView.width + spreadIndex * spreadView.tileDistance;
510 }
568 }
511
569
512− application: root.applicationManager.get(index)
570+ application: model.application
571+ surface: model.surface
513 closeable: !isDash
572 closeable: !isDash
514
573
515 property real behavioredIndex: index
574 property real behavioredIndex: index
@@ -593,11 +652,7 @@ AbstractStage {
593
652
594 onClicked: {
653 onClicked: {
595 if (root.altTabEnabled && spreadView.phase == 2) {
654 if (root.altTabEnabled && spreadView.phase == 2) {
596− if (root.applicationManager.focusedApplicationId == root.applicationManager.get(index).appId) {
597− spreadView.snapTo(index);
655+ spreadView.snapTo(index);
598− } else {
599− root.applicationManager.requestFocusApplication(root.applicationManager.get(index).appId);
600− }
601 }
656 }
602 }
657 }
603
658
@@ -611,7 +666,15 @@ AbstractStage {
611
666
612 onClosed: {
667 onClosed: {
613 spreadView.closingIndex = index;
668 spreadView.closingIndex = index;
669+ if (appDelegate.surface) {
670+ appDelegate.surface.close();
671+ } else if (appDelegate.application) {
614− root.applicationManager.stopApplication(root.applicationManager.get(index).appId);
672+ root.applicationManager.stopApplication(appDelegate.application.appId);
673+ } else {
674+ // should never happen
675+ console.warn("Can't close topLevelSurfaceList entry as it has neither"
676+ + " a surface nor an application");
677+ }
615 }
678 }
616
679
617 Binding {
680 Binding {
@@ -629,7 +692,7 @@ AbstractStage {
629
692
630 StagedFullscreenPolicy {
693 StagedFullscreenPolicy {
631 id: fullscreenPolicy
694 id: fullscreenPolicy
632− application: appDelegate.application
695+ surface: model.surface
633 }
696 }
634
697
635 Connections {
698 Connections {
@@ -637,7 +700,7 @@ AbstractStage {
637 onStageAboutToBeUnloaded: fullscreenPolicy.active = false
700 onStageAboutToBeUnloaded: fullscreenPolicy.active = false
638 }
701 }
639 }
702 }
640− }
703+ } // Repeater {
641 }
704 }
642 }
705 }
643
706
qml/Stages/PromptSurfaceAnimations.qmlnew file+81
@@ -0,0 +1,81 @@
1+/*
2+ * Copyright 2016 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+*/
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+
20+StateGroup {
21+ id: root
22+ property var container
23+ property var surfaceItem
24+
25+ states: [
26+ State {
27+ name: "blank"
28+ when: !root.surfaceItem.surface
29+ },
30+ State {
31+ name: "ready"
32+ when: root.surfaceItem.surface && root.surfaceItem.live
33+ },
34+ State {
35+ name: "zombie"
36+ when: root.surfaceItem.surface && !root.surfaceItem.live
37+ }
38+ ]
39+ transitions: [
40+ Transition {
41+ from: "*"; to: "zombie"
42+ // Slide downwards until it's out of view, through the bottom of the window
43+ SequentialAnimation {
44+ // clip so we don't go out of parent's bounds during spread
45+ PropertyAction { target: root.container.parent; property: "clip"; value: true }
46+ UbuntuNumberAnimation { target: root.surfaceItem; property: "y"; to: root.container.height
47+ duration: UbuntuAnimation.BriskDuration }
48+ PropertyAction { target: root.surfaceItem; property: "visible"; value: false }
49+ PropertyAction { target: container.parent; property: "clip"; value: false }
50+ ScriptAction { script: {
51+ // Unity.Application can't destroy a zombie MirSurface if it's still being
52+ // referenced by a MirSurfaceItem.
53+ root.surfaceItem.surface = null;
54+ } }
55+ }
56+ },
57+ Transition {
58+ from: "*"; to: "ready"
59+ // Slide upwards into view, from the bottom of the window
60+ SequentialAnimation {
61+ // clip so we don't go out of parent's bounds during spread
62+ PropertyAction { target: root.container.parent; property: "clip"; value: true }
63+ ScriptAction { script: {
64+ root.surfaceItem.y = root.container.height;
65+ root.surfaceItem.visible = true;
66+ } }
67+ UbuntuNumberAnimation {
68+ target: root.surfaceItem; property: "y"; to: 0
69+ duration: UbuntuAnimation.BriskDuration
70+ }
71+ PropertyAction { target: container.parent; property: "clip"; value: false }
72+ }
73+ },
74+ Transition {
75+ from: "*"; to: "blank"
76+ ScriptAction { script: {
77+ root.surfaceItem.visible = false;
78+ } }
79+ }
80+ ]
81+}
qml/Stages/SessionContainer.qmldeleted−180
@@ -1,180 +0,0 @@
1−/*
2− * Copyright 2014-2015 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU Lesser General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15−*/
16−
17−import QtQuick 2.4
18−import "Animations"
19−
20−FocusScope {
21− id: root
22− objectName: "sessionContainer"
23− implicitWidth: _surfaceContainer.implicitWidth
24− implicitHeight: _surfaceContainer.implicitHeight
25− property QtObject session
26− readonly property var childSessions: session ? session.childSessions : null
27− readonly property alias surface: _surfaceContainer.surface
28− property alias interactive: _surfaceContainer.interactive
29− property alias surfaceOrientationAngle: _surfaceContainer.surfaceOrientationAngle
30− property alias resizeSurface: _surfaceContainer.resizeSurface
31−
32− property int requestedWidth: -1
33− property int requestedHeight: -1
34−
35− readonly property alias surfaceContainer: _surfaceContainer
36− SurfaceContainer {
37− id: _surfaceContainer
38− requestedWidth: root.requestedWidth
39− requestedHeight: root.requestedHeight
40− surface: session ? session.lastSurface : null
41− }
42−
43− // SurfaceContainer size drives SessionContainer size
44− Binding {
45− target: root; property: "width"; value: _surfaceContainer.width
46− when: root.requestedWidth >= 0
47− }
48− Binding {
49− target: root; property: "height"; value: _surfaceContainer.height
50− when: root.requestedHeight >= 0
51− }
52−
53− // SessionContainer size drives SurfaceContainer size
54− Binding {
55− target: _surfaceContainer; property: "width"; value: root.width
56− when: root.requestedWidth < 0
57− }
58− Binding {
59− target: _surfaceContainer; property: "height"; value: root.height
60− when: root.requestedHeight < 0
61− }
62−
63− Repeater {
64− id: childSessionsRepeater
65− model: root.childSessions
66−
67− delegate: Loader {
68− objectName: "childDelegate" + index
69− anchors.fill: surfaceContainer
70−
71− // Only way to do recursive qml items.
72− source: Qt.resolvedUrl("SessionContainer.qml")
73−
74− z: index
75−
76− // Since a Loader is a FocusScope, propagate its focus to the loaded Item
77− Binding {
78− target: item; when: item
79− property: "focus"; value: focus
80− }
81−
82− Binding {
83− target: item; when: item
84− property: "interactive"; value: index == (childSessionsRepeater.count - 1) && root.interactive
85− }
86−
87− Binding {
88− target: item; when: item
89− property: "session"; value: modelData
90− }
91−
92− Binding {
93− target: item; when: item
94− property: "width"; value: root.width
95− }
96−
97− Binding {
98− target: item; when: item
99− property: "height"; value: root.height
100− }
101− }
102− }
103−
104− states: [
105− State {
106− name: "rootSession"
107− when: root.session && !root.session.parentSession
108− },
109−
110− State {
111− name: "childSession"
112− when: root.session && root.session.parentSession !== null && root.session.live
113− && !root.session.lastSurface
114− },
115−
116− State {
117− name: "childSessionReady"
118− when: root.session && root.session.parentSession !== null && root.session.live
119− && root.session.lastSurface !== null
120− },
121−
122− State {
123− name: "childSessionZombie"
124− when: root.session && root.session.parentSession !== null && !root.session.live
125− }
126− ]
127−
128− transitions: [
129− Transition {
130− to: "childSessionReady"
131− ScriptAction { script: { if (!surfaceContainer.hadSurface) { animateIn(swipeFromBottom); } } }
132− },
133− Transition {
134− to: "childSessionZombie"
135− ScriptAction { script: { animateOut(); } }
136− }
137− ]
138−
139− function animateIn(component) {
140− var animation = component.createObject(root, { "container": root, });
141− animation.start();
142−
143− var tmp = d.animations;
144− tmp.push(animation);
145− d.animations = tmp;
146− }
147−
148− function animateOut() {
149− if (d.animations.length > 0) {
150− var tmp = d.animations;
151− var popped = tmp.pop();
152− popped.completed.connect(function() { root.session.release(); } );
153− popped.end();
154− d.animations = tmp;
155− } else {
156− root.session.release();
157− }
158− }
159−
160− Component {
161− id: swipeFromBottom
162− SwipeFromBottomAnimation {}
163− }
164−
165− QtObject {
166− id: d
167− property var animations: []
168−
169− property var focusedChild: {
170− if (childSessionsRepeater.count == 0) {
171− return _surfaceContainer;
172− } else {
173− return childSessionsRepeater.itemAt(childSessionsRepeater.count - 1);
174− }
175− }
176− onFocusedChildChanged: {
177− focusedChild.focus = true;
178− }
179− }
180−}
qml/Stages/SpreadDelegate.qml+4−3
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -45,6 +45,7 @@ FocusScope {
45 property alias swipeToCloseEnabled: dragArea.enabled
45 property alias swipeToCloseEnabled: dragArea.enabled
46 property bool closeable
46 property bool closeable
47 property alias application: appWindow.application
47 property alias application: appWindow.application
48+ property alias surface: appWindow.surface
48 property int shellOrientationAngle
49 property int shellOrientationAngle
49 property int shellOrientation
50 property int shellOrientation
50 property QtObject orientations
51 property QtObject orientations
@@ -250,7 +251,7 @@ FocusScope {
250 }
251 }
251 PropertyChanges {
252 PropertyChanges {
252 target: appWindow
253 target: appWindow
253− surfaceOrientationAngle: orientationAngle
254+ surfaceOrientationAngle: appWindowWithShadow.orientationAngle
254 }
255 }
255 },
256 },
256 State {
257 State {
@@ -292,7 +293,7 @@ FocusScope {
292
293
293 ApplicationWindow {
294 ApplicationWindow {
294 id: appWindow
295 id: appWindow
295− objectName: application ? "appWindow_" + application.appId : "appWindow_null"
296+ objectName: "appWindow"
296 focus: true
297 focus: true
297 anchors {
298 anchors {
298 fill: parent
299 fill: parent
qml/Stages/StagedFullscreenPolicy.qml+12−14
@@ -27,31 +27,29 @@ import Unity.Application 0.1
27// Chrome not set and state change to fulscreen -> client window stays "fullscreen"
27// Chrome not set and state change to fulscreen -> client window stays "fullscreen"
28QtObject {
28QtObject {
29 property bool active: true
29 property bool active: true
30− property QtObject application: null
31
30
32− readonly property var lastSurface: application && application.session ?
31+ property var surface: null
33− application.session.lastSurface : null
32+ onSurfaceChanged: {
34− onLastSurfaceChanged: {
35− if (!active || !lastSurface) return;
33+ if (!active || !surface) return;
36− if (lastSurface.shellChrome === Mir.LowChrome) {
34+ if (surface.shellChrome === Mir.LowChrome) {
37− lastSurface.state = Mir.FullscreenState;
35+ surface.state = Mir.FullscreenState;
38 }
36 }
39 }
37 }
40
38
41 property var _connections: Connections {
39 property var _connections: Connections {
42− target: lastSurface
40+ target: surface
43 onShellChromeChanged: {
41 onShellChromeChanged: {
44− if (!active || !lastSurface) return;
42+ if (!active || !surface) return;
45− if (lastSurface.shellChrome === Mir.LowChrome) {
43+ if (surface.shellChrome === Mir.LowChrome) {
46− lastSurface.state = Mir.FullscreenState;
44+ surface.state = Mir.FullscreenState;
47 } else {
45 } else {
48− lastSurface.state = Mir.RestoredState;
46+ surface.state = Mir.RestoredState;
49 }
47 }
50 }
48 }
51 onStateChanged: {
49 onStateChanged: {
52 if (!active) return;
50 if (!active) return;
53− if (lastSurface.state === Mir.RestoredState && lastSurface.shellChrome === Mir.LowChrome) {
51+ if (surface.state === Mir.RestoredState && surface.shellChrome === Mir.LowChrome) {
54− lastSurface.state = Mir.FullscreenState;
52+ surface.state = Mir.FullscreenState;
55 }
53 }
56 }
54 }
57 }
55 }
qml/Stages/SurfaceContainer.qml+97−40
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -24,21 +24,22 @@ FocusScope {
24 id: root
24 id: root
25 objectName: "surfaceContainer"
25 objectName: "surfaceContainer"
26
26
27+ // Must be set from outside
27 property var surface: null
28 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
33
29
30+ // Might be changed from outside
34 property int requestedWidth: -1
31 property int requestedWidth: -1
35 property int requestedHeight: -1
32 property int requestedHeight: -1
33+ property bool interactive
34+ property int surfaceOrientationAngle: 0
35+ property bool resizeSurface: true
36+ property bool inPromptSession: false
36
37
37 onSurfaceChanged: {
38 onSurfaceChanged: {
38− if (surface) {
39+ // Not a binding because animations might remove the surface from the surfaceItem
39− surfaceItem.surface = surface;
40+ // programatically (in order to signal that a zombie surface is free for deletion),
40− root.hadSurface = false;
41+ // even though root.surface is still !null.
41− }
42+ surfaceItem.surface = surface;
42 }
43 }
43
44
44 InputWatcher {
45 InputWatcher {
@@ -83,11 +84,16 @@ FocusScope {
83 }
84 }
84
85
85 enabled: root.interactive
86 enabled: root.interactive
86− focus: true
87 antialiasing: !root.interactive
87 antialiasing: !root.interactive
88 orientationAngle: root.surfaceOrientationAngle
88 orientationAngle: root.surfaceOrientationAngle
89 }
89 }
90
90
91+ TouchGate {
92+ targetItem: surfaceItem
93+ anchors.fill: root
94+ enabled: surfaceItem.enabled
95+ }
96+
91 // MirSurface size drives SurfaceContainer size
97 // MirSurface size drives SurfaceContainer size
92 Binding {
98 Binding {
93 target: surfaceItem; property: "width"; value: root.surface ? root.surface.size.width : 0
99 target: surfaceItem; property: "width"; value: root.surface ? root.surface.size.width : 0
@@ -116,39 +122,90 @@ FocusScope {
116 when: root.requestedHeight < 0
122 when: root.requestedHeight < 0
117 }
123 }
118
124
125+ Repeater {
126+ id: childSurfacesRepeater
127+ objectName: "childSurfacesRepeater"
128+ model: root.surface ? root.surface.promptSurfaceList : null
119
129
120− TouchGate {
130+ delegate: Loader {
121− objectName: "touchGate-"+name
131+ objectName: "childDelegate" + index
122− targetItem: surfaceItem
123− anchors.fill: root
132+ anchors.fill: root
124− enabled: surfaceItem.enabled
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+ }
125 }
170 }
126
171
127− states: [
172+ Loader {
128− State {
173+ id: animationsLoader
129− name: "zombie"
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"
130− when: surfaceItem.surface && !surfaceItem.live
188+ value: surfaceItem
189+ }
190+ Binding {
191+ target: animationsLoader.item
192+ when: animationsLoader.item
193+ property: "container"
194+ value: root
131 }
195 }
132− ]
196+ }
133− transitions: [
197+
134− Transition {
198+ QtObject {
135− from: ""; to: "zombie"
199+ id: d
136− SequentialAnimation {
200+ property var focusedChild: {
137− UbuntuNumberAnimation { target: surfaceItem; property: "opacity"; to: 0.0
201+ if (childSurfacesRepeater.count == 0) {
138− duration: UbuntuAnimation.BriskDuration }
202+ return surfaceItem;
139− PropertyAction { target: surfaceItem; property: "visible"; value: false }
203+ } else {
140− ScriptAction { script: {
204+ return childSurfacesRepeater.itemAt(childSurfacesRepeater.count - 1);
141− surfaceItem.surface = null;
142− root.hadSurface = true;
143− } }
144 }
205 }
145− },
146− Transition {
147− from: "zombie"; to: ""
148− ScriptAction { script: {
149− surfaceItem.opacity = 1.0;
150− surfaceItem.visible = true;
151− } }
152 }
206 }
153− ]
207+ onFocusedChildChanged: {
208+ focusedChild.focus = true;
209+ }
210+ }
154}
211}
qml/Stages/TabletStage.qml+300−237
@@ -27,19 +27,24 @@ AbstractStage {
27 objectName: "stages"
27 objectName: "stages"
28 anchors.fill: parent
28 anchors.fill: parent
29
29
30+ // <tutorial-hacks> The Tutorial looks into our implementation details
30 property alias sideStageVisible: spreadView.sideStageVisible
31 property alias sideStageVisible: spreadView.sideStageVisible
31 property alias sideStageWidth: spreadView.sideStageWidth
32 property alias sideStageWidth: spreadView.sideStageWidth
33+ // The stage the currently focused surface is in
34+ property int stageFocusedSurface: priv.focusedAppDelegate ? priv.focusedAppDelegate.stage : ApplicationInfoInterface.MainStage
35+ // </tutorial-hacks>
32
36
33 // Functions to be called from outside
37 // Functions to be called from outside
34 function updateFocusedAppOrientation() {
38 function updateFocusedAppOrientation() {
35− var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
39+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
36− if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
40+
41+ if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
37− spreadRepeater.itemAt(mainStageAppIndex).matchShellOrientation();
42+ spreadRepeater.itemAt(mainStageIndex).matchShellOrientation();
38 }
43 }
39
44
40 for (var i = 0; i < spreadRepeater.count; ++i) {
45 for (var i = 0; i < spreadRepeater.count; ++i) {
41
46
42− if (i === mainStageAppIndex) {
47+ if (i === mainStageIndex) {
43 continue;
48 continue;
44 }
49 }
45
50
@@ -60,16 +65,14 @@ AbstractStage {
60 }
65 }
61 }
66 }
62 function updateFocusedAppOrientationAnimated() {
67 function updateFocusedAppOrientationAnimated() {
63− var mainStageAppIndex = priv.indexOf(priv.mainStageAppId);
68+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
64− if (mainStageAppIndex >= 0 && mainStageAppIndex < spreadRepeater.count) {
69+ if (priv.mainStageItemId && mainStageIndex >= 0 && mainStageIndex < spreadRepeater.count) {
65− spreadRepeater.itemAt(mainStageAppIndex).animateToShellOrientation();
70+ spreadRepeater.itemAt(mainStageIndex).animateToShellOrientation();
66 }
71 }
67
72
68− if (priv.sideStageAppId) {
73+ var sideStageIndex = root.topLevelSurfaceList.indexForId(priv.sideStageItemId);
69− var sideStageAppIndex = priv.indexOf(priv.sideStageAppId);
70− if (sideStageAppIndex >= 0 && sideStageAppIndex < spreadRepeater.count) {
74+ if (sideStageIndex >= 0 && sideStageIndex < spreadRepeater.count) {
71− spreadRepeater.itemAt(sideStageAppIndex).matchShellOrientation();
75+ spreadRepeater.itemAt(sideStageIndex).matchShellOrientation();
72− }
73 }
76 }
74 }
77 }
75
78
@@ -81,11 +84,20 @@ AbstractStage {
81
84
82 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
85 orientationChangesEnabled: priv.mainAppOrientationChangesEnabled
83
86
87+ mainApp: {
88+ if (priv.mainStageItemId > 0) {
89+ var index = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
90+ return root.topLevelSurfaceList.applicationAt(index);
91+ } else {
92+ return null;
93+ }
94+ }
95+
84 supportedOrientations: {
96 supportedOrientations: {
85 if (mainApp) {
97 if (mainApp) {
86 var orientations = mainApp.supportedOrientations;
98 var orientations = mainApp.supportedOrientations;
87 orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
99 orientations |= Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation;
88− if (priv.sideStageAppId && !spreadView.surfaceDragging) {
100+ if (priv.sideStageItemId && !spreadView.surfaceDragging) {
89 // If we have a sidestage app, support Portrait orientation
101 // If we have a sidestage app, support Portrait orientation
90 // so that it will switch the sidestage app to mainstage on rotate
102 // so that it will switch the sidestage app to mainstage on rotate
91 orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
103 orientations |= Qt.PortraitOrientation|Qt.InvertedPortraitOrientation;
@@ -116,7 +128,7 @@ AbstractStage {
116 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
128 if (inverseProgress == 0 && priv.oldInverseProgress > 0) {
117 // left edge drag released. Minimum distance is given by design.
129 // left edge drag released. Minimum distance is given by design.
118 if (priv.oldInverseProgress > units.gu(22)) {
130 if (priv.oldInverseProgress > units.gu(22)) {
119− ApplicationManager.requestFocusApplication("unity8-dash");
131+ root.applicationManager.requestFocusApplication("unity8-dash");
120 }
132 }
121 }
133 }
122 priv.oldInverseProgress = inverseProgress;
134 priv.oldInverseProgress = inverseProgress;
@@ -154,17 +166,51 @@ AbstractStage {
154 }
166 }
155 }
167 }
156
168
169+ Connections {
170+ target: root.topLevelSurfaceList
171+ onListChanged: priv.updateMainAndSideStageIndexes()
172+ }
173+
157 QtObject {
174 QtObject {
158 id: priv
175 id: priv
159 objectName: "stagesPriv"
176 objectName: "stagesPriv"
160
177
161− property string focusedAppId: ApplicationManager.focusedApplicationId
178+ function updateMainAndSideStageIndexes() {
162− readonly property var focusedAppDelegate: {
179+ var choseMainStage = false;
163− var index = indexOf(focusedAppId);
180+ var choseSideStage = false;
164− return index >= 0 && index < spreadRepeater.count ? spreadRepeater.itemAt(index) : null
181+
182+ if (!root.topLevelSurfaceList)
183+ return;
184+
185+ for (var i = 0; i < spreadRepeater.count && (!choseMainStage || !choseSideStage); ++i) {
186+ var spreadDelegate = spreadRepeater.itemAt(i);
187+ if (sideStage.shown && spreadDelegate.stage == ApplicationInfoInterface.SideStage
188+ && !choseSideStage) {
189+ priv.sideStageDelegate = spreadDelegate
190+ priv.sideStageItemId = root.topLevelSurfaceList.idAt(i);
191+ priv.sideStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
192+ choseSideStage = true;
193+ } else if (!choseMainStage && spreadDelegate.stage == ApplicationInfoInterface.MainStage) {
194+ priv.mainStageDelegate = spreadDelegate;
195+ priv.mainStageItemId = root.topLevelSurfaceList.idAt(i);
196+ priv.mainStageAppId = root.topLevelSurfaceList.applicationAt(i).appId;
197+ choseMainStage = true;
198+ }
199+ }
200+ if (!choseMainStage) {
201+ priv.mainStageDelegate = null;
202+ priv.mainStageItemId = 0;
203+ priv.mainStageAppId = "";
204+ }
205+ if (!choseSideStage) {
206+ priv.sideStageDelegate = null;
207+ priv.sideStageItemId = 0;
208+ priv.sideStageAppId = "";
209+ }
165 }
210 }
166
211
167− property string oldFocusedAppId: ""
212+ property var focusedAppDelegate: null
213+
168 property bool mainAppOrientationChangesEnabled: false
214 property bool mainAppOrientationChangesEnabled: false
169
215
170 property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
216 property real landscapeHeight: root.orientations.native_ == Qt.LandscapeOrientation ?
@@ -173,36 +219,21 @@ AbstractStage {
173 property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
219 property bool shellIsLandscape: root.shellOrientation === Qt.LandscapeOrientation
174 || root.shellOrientation === Qt.InvertedLandscapeOrientation
220 || root.shellOrientation === Qt.InvertedLandscapeOrientation
175
221
176− property string mainStageAppId
222+ property var mainStageDelegate: null
177− property string sideStageAppId
223+ property var sideStageDelegate: null
178
224
179− // For convenience, keep properties of the first two apps in the model
225+ property int mainStageItemId: 0
226+ property int sideStageItemId: 0
227+
180− property string appId0
228+ property string mainStageAppId: ""
181− property string appId1
229+ property string sideStageAppId: ""
182
230
183 property int oldInverseProgress: 0
231 property int oldInverseProgress: 0
184
232
185 property int highlightIndex: 0
233 property int highlightIndex: 0
186
234
187− onFocusedAppIdChanged: updateStageApps()
188−
189− onFocusedAppDelegateChanged: {
190− if (focusedAppDelegate) {
191− focusedAppDelegate.focus = true;
192− }
193− }
194−
195 property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
235 property bool focusedAppDelegateIsDislocated: focusedAppDelegate &&
196 (focusedAppDelegate.dragOffset !== 0 || focusedAppDelegate.xTranslateAnimating)
236 (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−
206 function evaluateOneWayFlick(gesturePoints) {
237 function evaluateOneWayFlick(gesturePoints) {
207 // Need to have at least 3 points to recognize it as a flick
238 // Need to have at least 3 points to recognize it as a flick
208 if (gesturePoints.length < 3) {
239 if (gesturePoints.length < 3) {
@@ -231,88 +262,45 @@ AbstractStage {
231 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
262 spreadView.contentX = highlightIndex * spreadView.contentWidth / (spreadRepeater.count + 2)
232 }
263 }
233
264
234− function getTopApp(stage) {
235− for (var i = 0; i < ApplicationManager.count; i++) {
236− var app = ApplicationManager.get(i)
237− if (app.stage === stage) {
238− return app;
239− }
240− }
241− return null;
242− }
243−
244− function setAppStage(appId, stage, save) {
245− var app = ApplicationManager.findApplication(appId);
246− if (app) {
247− app.stage = stage;
248− if (save) {
249− WindowStateStorage.saveStage(appId, stage);
250− }
251− }
252− }
253−
254− function updateStageApps() {
255− var app = priv.getTopApp(ApplicationInfoInterface.MainStage);
256− priv.mainStageAppId = app ? app.appId : ""
257− root.mainApp = app;
258−
259− if (sideStage.shown) {
260− app = priv.getTopApp(ApplicationInfoInterface.SideStage);
261− priv.sideStageAppId = app ? app.appId : ""
262− } else {
263− priv.sideStageAppId = "";
264− }
265−
266− appId0 = ApplicationManager.count >= 1 ? ApplicationManager.get(0).appId : "";
267− appId1 = ApplicationManager.count > 1 ? ApplicationManager.get(1).appId : "";
268− }
269−
270 readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
265 readonly property bool sideStageEnabled: root.shellOrientation == Qt.LandscapeOrientation ||
271 root.shellOrientation == Qt.InvertedLandscapeOrientation
266 root.shellOrientation == Qt.InvertedLandscapeOrientation
272− Component.onCompleted: updateStageApps();
273 }
267 }
274
268
275− Connections {
269+ Instantiator {
276− target: ApplicationManager
270+ model: root.applicationManager
277− onFocusRequested: {
271+ delegate: QtObject {
278− if (spreadView.interactive) {
272+ property var stateBinding: Binding {
279− spreadView.snapTo(priv.indexOf(appId));
273+ readonly property bool isDash: model.application ? model.application.appId == "unity8-dash" : false
280− } else {
274+ target: model.application
281− ApplicationManager.focusApplication(appId);
275+ property: "requestedState"
276+
277+ // NB: the first application clause is just to ensure we never get warnings for trying to access
278+ // members of a null variable.
279+ value: model.application &&
280+ (
281+ (isDash && root.keepDashRunning)
282+ || (!root.suspended && (model.application.appId === priv.mainStageAppId
283+ || model.application.appId === priv.sideStageAppId))
284+ )
285+ ? ApplicationInfoInterface.RequestedRunning
286+ : ApplicationInfoInterface.RequestedSuspended
282 }
287 }
283− }
284
288
285− onApplicationAdded: {
289+ property var lifecycleBinding: Binding {
286− if (spreadView.phase == 2) {
290+ target: model.application
287− spreadView.snapTo(ApplicationManager.count - 1);
291+ property: "exemptFromLifecycle"
288− } else {
292+ value: model.application
289− spreadView.phase = 0;
293+ ? (!model.application.isTouchApp || isExemptFromLifecycle(model.application.appId))
290− spreadView.contentX = -spreadView.shift;
294+ : false
291− ApplicationManager.focusApplication(appId);
292 }
295 }
293 }
296 }
297+ }
294
298
295− onApplicationRemoved: {
299+ Binding {
296− if (priv.mainStageAppId == appId) {
300+ target: MirFocusController
297− ApplicationManager.focusApplication("unity8-dash")
301+ property: "focusedSurface"
298− }
302+ value: priv.focusedAppDelegate ? priv.focusedAppDelegate.surface : null
299− if (priv.sideStageAppId == appId) {
303+ when: root.parent && !spreadRepeater.startingUp
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− }
316 }
304 }
317
305
318 Flickable {
306 Flickable {
@@ -363,16 +351,45 @@ AbstractStage {
363 property int selectedIndex: -1
351 property int selectedIndex: -1
364 property int draggedDelegateCount: 0
352 property int draggedDelegateCount: 0
365 property int closingIndex: -1
353 property int closingIndex: -1
366− property var selectedApplication: selectedIndex !== -1 ? ApplicationManager.get(selectedIndex) : null
354+ property var selectedDelegate: selectedIndex !== -1 ? spreadRepeater.itemAt(selectedIndex) : null
367−
355+
368− // FIXME: Workaround Flickable's not keepping its contentX still when resized
356+ // <FIXME-contentX> Workaround Flickable's behavior of bringing contentX back between valid boundaries
357+ // when resized. The proper way to fix this is refactoring PhoneStage so that it doesn't
358+ // rely on having Flickable.contentX keeping an out-of-bounds value when it's set programatically
359+ // (as opposed to having contentX reaching an out-of-bounds value through dragging, which will trigger
360+ // the Flickable.boundsBehavior upon release).
361+ onContentXChanged: {
362+ if (!undoContentXReset()) {
369− onContentXChanged: { forceItToRemainStillIfBeingResized(); }
363+ forceItToRemainStillIfBeingResized();
364+ }
365+ }
370 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
366 onShiftChanged: { forceItToRemainStillIfBeingResized(); }
371 function forceItToRemainStillIfBeingResized() {
367 function forceItToRemainStillIfBeingResized() {
372− if (root.beingResized && contentX != -shift) {
368+ if (root.beingResized && contentX != -spreadView.shift) {
369+ contentX = -spreadView.shift;
370+ }
371+ }
372+ function undoContentXReset() {
373+ if (contentWidth <= 0) {
374+ contentWidthOnLastContentXChange = contentWidth;
375+ lastContentX = contentX;
376+ return false;
377+ }
378+
379+ if (contentWidth != contentWidthOnLastContentXChange
380+ && lastContentX == -shift && contentX == 0) {
381+ // Flickable is resetting contentX because contentWidth has changed. Undo it.
373 contentX = -shift;
382 contentX = -shift;
383+ return true;
374 }
384 }
385+
386+ contentWidthOnLastContentXChange = contentWidth;
387+ lastContentX = contentX;
388+ return false;
375 }
389 }
390+ property real contentWidthOnLastContentXChange: -1
391+ property real lastContentX: 0
392+ // </FIXME-contentX>
376
393
377 property bool animateX: true
394 property bool animateX: true
378 property bool beingResized: root.beingResized
395 property bool beingResized: root.beingResized
@@ -385,31 +402,33 @@ AbstractStage {
385 }
402 }
386 }
403 }
387
404
388− property real sideStageDragProgress: sideStage.progress
389− property bool sideStageVisible: priv.sideStageAppId
390 property real sideStageWidth: units.gu(40)
405 property real sideStageWidth: units.gu(40)
391
406
392 property bool surfaceDragging: triGestureArea.recognisedDrag
407 property bool surfaceDragging: triGestureArea.recognisedDrag
393
408
409+ readonly property bool sideStageVisible: priv.sideStageItemId != 0
410+
394− // In case the ApplicationManager already holds an app when starting up we're missing animations
411+ // In case applicationManager already holds an app when starting up we're missing animations
395 // Make sure we end up in the same state
412 // Make sure we end up in the same state
396 Component.onCompleted: {
413 Component.onCompleted: {
397 spreadView.contentX = -spreadView.shift
414 spreadView.contentX = -spreadView.shift
398 }
415 }
399
416
400 property int nextInStack: {
417 property int nextInStack: {
418+ var mainStageIndex = priv.mainStageDelegate ? priv.mainStageDelegate.index : -1;
419+ var sideStageIndex = priv.sideStageDelegate ? priv.sideStageDelegate.index : -1;
401 switch (state) {
420 switch (state) {
402 case "main":
421 case "main":
403− if (ApplicationManager.count > 1) {
422+ if (root.topLevelSurfaceList.count > 1) {
404 return 1;
423 return 1;
405 }
424 }
406 return -1;
425 return -1;
407 case "mainAndOverlay":
426 case "mainAndOverlay":
408− if (ApplicationManager.count <= 2) {
427+ if (root.topLevelSurfaceList.count <= 2) {
409 return -1;
428 return -1;
410 }
429 }
411− if (priv.appId0 == priv.mainStageAppId || priv.appId0 == priv.sideStageAppId) {
430+ if (mainStageIndex == 0 || sideStageIndex == 0) {
412− if (priv.appId1 == priv.mainStageAppId || priv.appId1 == priv.sideStageAppId) {
431+ if (mainStageIndex == 1 || sideStageIndex == 1) {
413 return 2;
432 return 2;
414 }
433 }
415 return 1;
434 return 1;
@@ -420,7 +439,7 @@ AbstractStage {
420 }
439 }
421 return -1;
440 return -1;
422 }
441 }
423− property int nextZInStack: indexToZIndex(nextInStack)
442+ property int nextZInStack
424
443
425 states: [
444 states: [
426 State {
445 State {
@@ -440,13 +459,13 @@ AbstractStage {
440 }
459 }
441 ]
460 ]
442 state: {
461 state: {
443− if ((priv.mainStageAppId && !priv.sideStageAppId) || !priv.sideStageEnabled) {
462+ if ((priv.mainStageItemId && !priv.sideStageItemId) || !priv.sideStageEnabled) {
444 return "main";
463 return "main";
445 }
464 }
446− if (!priv.mainStageAppId && priv.sideStageAppId) {
465+ if (!priv.mainStageItemId && priv.sideStageItemId) {
447 return "overlay";
466 return "overlay";
448 }
467 }
449− if (priv.mainStageAppId && priv.sideStageAppId) {
468+ if (priv.mainStageItemId && priv.sideStageItemId) {
450 return "mainAndOverlay";
469 return "mainAndOverlay";
451 }
470 }
452 return "empty";
471 return "empty";
@@ -511,38 +530,41 @@ AbstractStage {
511 // only shuffle when we've got a main and overlay
530 // only shuffle when we've got a main and overlay
512 if (state !== "mainAndOverlay") return index;
531 if (state !== "mainAndOverlay") return index;
513
532
514− var app = ApplicationManager.get(index);
533+ var app = root.topLevelSurfaceList.applicationAt(index);
515 if (!app) {
534 if (!app) {
516 return index;
535 return index;
517 }
536 }
537+ var stage = spreadRepeater.itemAt(index) ? spreadRepeater.itemAt(index).stage : app.stage;
518
538
519 // don't shuffle indexes greater than "actives or next"
539 // don't shuffle indexes greater than "actives or next"
520 if (index > 2) return index;
540 if (index > 2) return index;
521
541
522− if (app.appId === priv.mainStageAppId) {
542+ var mainStageIndex = root.topLevelSurfaceList.indexForId(priv.mainStageItemId);
543+
544+ if (index == mainStageIndex) {
523 // Active main stage always at 0
545 // Active main stage always at 0
524 return 0;
546 return 0;
525 }
547 }
526
548
527 if (spreadView.nextInStack > 0) {
549 if (spreadView.nextInStack > 0) {
528− var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
550+ var stageOfNextInStack = spreadRepeater.itemAt(spreadView.nextInStack).stage;
529
551
530 if (index === spreadView.nextInStack) {
552 if (index === spreadView.nextInStack) {
531 // this is the next app in stack.
553 // this is the next app in stack.
532
554
533− if (app.stage === ApplicationInfoInterface.SideStage) {
555+ if (stage === ApplicationInfoInterface.SideStage) {
534 // if the next app in stack is a sidestage app, it must order on top of other side stage app
556 // if the next app in stack is a sidestage app, it must order on top of other side stage app
535− return Math.min(2, ApplicationManager.count-1);
557+ return Math.min(2, root.topLevelSurfaceList.count-1);
536 }
558 }
537 return 1;
559 return 1;
538 }
560 }
539− if (nextAppInStack.stage === ApplicationInfoInterface.SideStage) {
561+ if (stageOfNextInStack === ApplicationInfoInterface.SideStage) {
540 // if the next app in stack is a sidestage app, it must order on top of other side stage app
562 // if the next app in stack is a sidestage app, it must order on top of other side stage app
541 return 1;
563 return 1;
542 }
564 }
543− return Math.min(2, ApplicationManager.count-1);
565+ return Math.min(2, root.topLevelSurfaceList.count-1);
544 }
566 }
545− return Math.min(index+1, ApplicationManager.count-1);
567+ return Math.min(index+1, root.topLevelSurfaceList.count-1);
546 }
568 }
547
569
548 SequentialAnimation {
570 SequentialAnimation {
@@ -560,12 +582,13 @@ AbstractStage {
560 script: {
582 script: {
561 if (spreadView.selectedIndex >= 0) {
583 if (spreadView.selectedIndex >= 0) {
562 var newIndex = spreadView.selectedIndex;
584 var newIndex = spreadView.selectedIndex;
563− var application = ApplicationManager.get(newIndex);
585+ var application = root.topLevelSurfaceList.applicationAt(newIndex);
586+ var spreadDelegate = spreadRepeater.itemAt(newIndex);
564− if (application.stage === ApplicationInfoInterface.SideStage) {
587+ if (spreadDelegate.stage === ApplicationInfoInterface.SideStage) {
565 sideStage.showNow();
588 sideStage.showNow();
566 }
589 }
567 spreadView.selectedIndex = -1;
590 spreadView.selectedIndex = -1;
568− ApplicationManager.focusApplication(application.appId);
591+ spreadDelegate.focus = true;
569 spreadView.phase = 0;
592 spreadView.phase = 0;
570 spreadView.contentX = -spreadView.shift;
593 spreadView.contentX = -spreadView.shift;
571 }
594 }
@@ -581,7 +604,7 @@ AbstractStage {
581 MouseArea {
604 MouseArea {
582 id: spreadRow
605 id: spreadRow
583 x: spreadView.contentX
606 x: spreadView.contentX
584− width: spreadView.width + Math.max(spreadView.width, ApplicationManager.count * spreadView.tileDistance)
607+ width: spreadView.width + Math.max(spreadView.width, root.topLevelSurfaceList.count * spreadView.tileDistance)
585 height: root.height
608 height: root.height
586
609
587 onClicked: {
610 onClicked: {
@@ -599,8 +622,8 @@ AbstractStage {
599 enabled: priv.sideStageEnabled
622 enabled: priv.sideStageEnabled
600
623
601 onDropped: {
624 onDropped: {
602− priv.setAppStage(drag.source.appId, ApplicationInfoInterface.MainStage, true);
625+ drop.source.spreadDelegate.stage = ApplicationInfoInterface.MainStage;
603− ApplicationManager.focusApplication(drag.source.appId);
626+ drop.source.spreadDelegate.focus = true;
604 }
627 }
605 keys: "SideStage"
628 keys: "SideStage"
606 }
629 }
@@ -611,12 +634,12 @@ AbstractStage {
611 height: priv.landscapeHeight
634 height: priv.landscapeHeight
612 x: spreadView.width - width
635 x: spreadView.width - width
613 z: {
636 z: {
614− if (!priv.mainStageAppId) return 0;
637+ if (!priv.mainStageItemId) return 0;
615
638
616− if (priv.sideStageAppId && spreadView.nextInStack > 0) {
639+ if (priv.sideStageItemId && spreadView.nextInStack > 0) {
617− var nextAppInStack = ApplicationManager.get(spreadView.nextInStack);
640+ var nextDelegateInStack = spreadRepeater.itemAt(spreadView.nextInStack);
618
641
619− if (nextAppInStack.stage === ApplicationInfoInterface.MainStage) {
642+ if (nextDelegateInStack.stage === ApplicationInfoInterface.MainStage) {
620 // if the next app in stack is a main stage app, put the sidestage on top of it.
643 // if the next app in stack is a main stage app, put the sidestage on top of it.
621 return 2;
644 return 2;
622 }
645 }
@@ -631,12 +654,11 @@ AbstractStage {
631 Behavior on opacity { UbuntuNumberAnimation {} }
654 Behavior on opacity { UbuntuNumberAnimation {} }
632
655
633 onShownChanged: {
656 onShownChanged: {
634− if (!shown && ApplicationManager.focusedApplicationId == priv.sideStageAppId) {
657+ if (!shown && priv.sideStageDelegate && priv.focusedAppDelegate === priv.sideStageDelegate
635− ApplicationManager.requestFocusApplication(priv.mainStageAppId);
658+ && priv.mainStageDelegate) {
636− }
659+ priv.mainStageDelegate.focus = true;
637− priv.updateStageApps();
660+ } else if (shown && priv.sideStageDelegate) {
638− if (shown && priv.sideStageAppId) {
661+ priv.sideStageDelegate.focus = true;
639− ApplicationManager.requestFocusApplication(priv.sideStageAppId);
640 }
662 }
641 }
663 }
642
664
@@ -655,8 +677,8 @@ AbstractStage {
655 }
677 }
656 onDropped: {
678 onDropped: {
657 if (drop.keys == "MainStage") {
679 if (drop.keys == "MainStage") {
658− priv.setAppStage(drop.source.appId, ApplicationInfoInterface.SideStage, true);
680+ drop.source.spreadDelegate.stage = ApplicationInfoInterface.SideStage;
659− ApplicationManager.requestFocusApplication(drop.source.appId);
681+ drop.source.spreadDelegate.focus = true;
660 }
682 }
661 }
683 }
662 drag {
684 drag {
@@ -669,41 +691,102 @@ AbstractStage {
669 }
691 }
670 }
692 }
671
693
672− Repeater {
694+ TopLevelSurfaceRepeater {
673 id: spreadRepeater
695 id: spreadRepeater
674 objectName: "spreadRepeater"
696 objectName: "spreadRepeater"
675− model: ApplicationManager
697+ model: root.topLevelSurfaceList
698+
699+ onItemAdded: {
700+ priv.updateMainAndSideStageIndexes();
701+ if (spreadView.phase == 2) {
702+ spreadView.snapTo(index);
703+ }
704+ }
705+
706+ onItemRemoved: {
707+ priv.updateMainAndSideStageIndexes();
708+ // Unless we're closing the app ourselves in the spread,
709+ // lets make sure the spread doesn't mess up by the changing app list.
710+ if (spreadView.closingIndex == -1) {
711+ spreadView.phase = 0;
712+ spreadView.contentX = -spreadView.shift;
713+ focusTopMostApp();
714+ }
715+ }
716+ function focusTopMostApp() {
717+ if (spreadRepeater.count > 0) {
718+ var topmostDelegate = spreadRepeater.itemAt(0);
719+ topmostDelegate.focus = true;
720+ }
721+ }
676
722
677 delegate: TransformedTabletSpreadDelegate {
723 delegate: TransformedTabletSpreadDelegate {
678 id: spreadTile
724 id: spreadTile
679− objectName: model.appId ? "tabletSpreadDelegate_" + model.appId
725+ objectName: "spreadDelegate_" + model.id
680− : "tabletSpreadDelegate_null";
726+
727+ readonly property int index: model.index
681 width: spreadView.width
728 width: spreadView.width
682 height: spreadView.height
729 height: spreadView.height
683− active: appId == priv.mainStageAppId || appId == priv.sideStageAppId
730+ active: model.id == priv.mainStageItemId || model.id == priv.sideStageItemId
684 zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
731 zIndex: selected && stage == ApplicationInfoInterface.MainStage ? 0 : spreadView.indexToZIndex(index)
732+ onZIndexChanged: {
733+ if (spreadView.nextInStack == model.index) {
734+ spreadView.nextZInStack = zIndex;
735+ }
736+ }
685 selected: spreadView.selectedIndex == index
737 selected: spreadView.selectedIndex == index
686 otherSelected: spreadView.selectedIndex >= 0 && !selected
738 otherSelected: spreadView.selectedIndex >= 0 && !selected
687− isInSideStage: priv.sideStageAppId === appId
739+ isInSideStage: priv.sideStageItemId == model.id
688 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
740 interactive: !spreadView.interactive && spreadView.phase === 0 && root.interactive
689 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
741 swipeToCloseEnabled: spreadView.interactive && !snapAnimation.running
690 maximizedAppTopMargin: root.maximizedAppTopMargin
742 maximizedAppTopMargin: root.maximizedAppTopMargin
743+ dragOffset: !isDash && model.id == priv.mainStageItemId && root.inverseProgress > 0
691− dragOffset: !isDash && appId == priv.mainStageAppId && root.inverseProgress > 0 && spreadView.phase === 0 ? root.inverseProgress : 0
744+ && spreadView.phase === 0 ? root.inverseProgress : 0
692− application: ApplicationManager.get(index)
745+ application: model.application
746+ surface: model.surface
693 closeable: !isDash
747 closeable: !isDash
694 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
748 highlightShown: root.altTabPressed && priv.highlightIndex == zIndex
695
749
696− readonly property bool wantsMainStage: model.stage == ApplicationInfoInterface.MainStage
750+ readonly property bool wantsMainStage: stage == ApplicationInfoInterface.MainStage
751+
752+ readonly property bool isDash: model.application.appId == "unity8-dash"
697
753
698− readonly property string appId: model.appId
754+ onFocusChanged: {
699− readonly property bool isDash: model.appId == "unity8-dash"
755+ if (focus && !spreadRepeater.startingUp) {
756+ priv.focusedAppDelegate = spreadTile;
757+ root.topLevelSurfaceList.raiseId(model.id);
758+ }
759+ if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
760+ sideStage.show();
761+ }
762+ }
763+ Connections {
764+ target: model.surface
765+ onFocusRequested: spreadTile.focus = true;
766+ }
767+ Connections {
768+ target: model.application
769+ onFocusRequested: {
770+ if (!model.surface) {
771+ // when an app has no surfaces, we assume there's only one entry representing it:
772+ // this delegate.
773+ spreadTile.focus = true;
774+ } else {
775+ // if the application has surfaces, focus request should be at surface-level.
776+ }
777+ }
778+ }
700
779
701− stage: model.stage
702 fullscreen: {
780 fullscreen: {
703− if (mainApp && stage === ApplicationInfoInterface.SideStage) {
781+ if (priv.mainStageDelegate && stage === ApplicationInfoInterface.SideStage) {
704− return mainApp.fullscreen;
782+ return priv.mainStageDelegate.fullscreen;
783+ } else if (surface) {
784+ return surface.state === Mir.FullscreenState;
785+ } else if (application) {
786+ return application.fullscreen;
787+ } else {
788+ return false;
705 }
789 }
706− return application ? application.fullscreen : false;
707 }
790 }
708
791
709 supportedOrientations: {
792 supportedOrientations: {
@@ -724,23 +807,6 @@ AbstractStage {
724 }
807 }
725 }
808 }
726
809
727−
728− Binding {
729− target: spreadTile.application
730− property: "exemptFromLifecycle"
731− value: !model.isTouchApp || isExemptFromLifecycle(model.appId)
732− }
733−
734− Binding {
735− target: spreadTile.application
736− property: "requestedState"
737− value: (isDash && root.keepDashRunning)
738− || (!root.suspended && (model.appId == priv.mainStageAppId
739− || model.appId == priv.sideStageAppId))
740− ? ApplicationInfoInterface.RequestedRunning
741− : ApplicationInfoInterface.RequestedSuspended
742− }
743−
744 // FIXME: A regular binding doesn't update any more after closing an app.
810 // FIXME: A regular binding doesn't update any more after closing an app.
745 // Using a Binding for now.
811 // Using a Binding for now.
746 Binding {
812 Binding {
@@ -760,34 +826,33 @@ AbstractStage {
760 onSideStageEnabledChanged: refreshStage()
826 onSideStageEnabledChanged: refreshStage()
761 }
827 }
762
828
829+ property bool _constructing: true;
830+ onStageChanged: {
831+ if (!_constructing) {
832+ priv.updateMainAndSideStageIndexes();
833+ }
834+ }
835+
763 Component.onCompleted: {
836 Component.onCompleted: {
837+ // a top level window is always the focused one when it first appears, unfocusing
838+ // any preexisting one
839+ focus = true;
764− refreshStage()
840+ refreshStage();
765− stageChanged.connect(priv.updateStageApps);
841+ _constructing = false;
842+ }
843+ Component.onDestruction: {
844+ WindowStateStorage.saveStage(model.application.appId, stage);
766 }
845 }
767
846
768 function refreshStage() {
847 function refreshStage() {
769− var stage = ApplicationInfoInterface.MainStage;
848+ var newStage = ApplicationInfoInterface.MainStage;
770 if (priv.sideStageEnabled) {
849 if (priv.sideStageEnabled) {
771 if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
850 if (application && application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
772− stage = WindowStateStorage.getStage(appId);
851+ newStage = WindowStateStorage.getStage(application.appId);
773 }
852 }
774 }
853 }
775
854
776− if (model.stage !== stage) {
855+ stage = newStage;
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− })
791 }
856 }
792
857
793 progress: {
858 progress: {
@@ -873,13 +938,10 @@ AbstractStage {
873 script: {
938 script: {
874 // rotate immediately.
939 // rotate immediately.
875 spreadTile.matchShellOrientation();
940 spreadTile.matchShellOrientation();
876− if (ApplicationManager.focusedApplicationId === spreadTile.appId &&
941+ if (priv.focusedAppDelegate === spreadTile &&
877 priv.sideStageEnabled && !sideStage.shown) {
942 priv.sideStageEnabled && !sideStage.shown) {
878 // Sidestage was focused, so show the side stage.
943 // Sidestage was focused, so show the side stage.
879 sideStage.show();
944 sideStage.show();
880− // if we've switched to a main app which doesnt support portrait, hide the side stage.
881− } else if (mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
882− sideStage.hideNow();
883 }
945 }
884 }
946 }
885 }
947 }
@@ -890,10 +952,10 @@ AbstractStage {
890 SequentialAnimation {
952 SequentialAnimation {
891 ScriptAction {
953 ScriptAction {
892 script: {
954 script: {
893− if (priv.sideStageAppId === spreadTile.appId &&
955+ if (priv.sideStageDelegate === spreadTile &&
894 mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
956 mainApp && (mainApp.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) == 0) {
895 // The mainstage app did not natively support portrait orientation, so focus the sidestage.
957 // The mainstage app did not natively support portrait orientation, so focus the sidestage.
896− ApplicationManager.requestFocusApplication(spreadTile.appId);
958+ spreadTile.focus = true;
897 }
959 }
898 }
960 }
899 }
961 }
@@ -922,28 +984,26 @@ AbstractStage {
922
984
923 onClosed: {
985 onClosed: {
924 spreadView.closingIndex = index;
986 spreadView.closingIndex = index;
925− ApplicationManager.stopApplication(ApplicationManager.get(index).appId);
987+ if (spreadTile.surface) {
926− }
988+ spreadTile.surface.close();
927−
989+ } else if (spreadTile.application) {
928− onFocusChanged: {
990+ root.applicationManager.stopApplication(spreadTile.application.appId);
929− if (focus && ApplicationManager.focusedApplicationId !== appId) {
991+ } else {
930− ApplicationManager.focusApplication(appId);
992+ // should never happen
931− }
993+ console.warn("Can't close topLevelSurfaceList entry as it has neither"
932−
994+ + " a surface nor an application");
933− if (focus && priv.sideStageEnabled && stage === ApplicationInfoInterface.SideStage) {
934− sideStage.show();
935 }
995 }
936 }
996 }
937
997
938 Binding {
998 Binding {
939 target: root
999 target: root
940− when: model.appId == priv.mainStageAppId
1000+ when: model.id == priv.mainStageItemId
941 property: "mainAppWindowOrientationAngle"
1001 property: "mainAppWindowOrientationAngle"
942 value: appWindowOrientationAngle
1002 value: appWindowOrientationAngle
943 }
1003 }
944 Binding {
1004 Binding {
945 target: priv
1005 target: priv
946− when: model.appId == priv.mainStageAppId
1006+ when: model.id == priv.mainStageItemId
947 property: "mainAppOrientationChangesEnabled"
1007 property: "mainAppOrientationChangesEnabled"
948 value: orientationChangesEnabled
1008 value: orientationChangesEnabled
949 }
1009 }
@@ -957,7 +1017,7 @@ AbstractStage {
957
1017
958 StagedFullscreenPolicy {
1018 StagedFullscreenPolicy {
959 id: fullscreenPolicy
1019 id: fullscreenPolicy
960− application: spreadTile.application
1020+ surface: model.surface
961 }
1021 }
962 Connections {
1022 Connections {
963 target: root
1023 target: root
@@ -973,17 +1033,19 @@ AbstractStage {
973 anchors.fill: parent
1033 anchors.fill: parent
974 enabled: priv.sideStageEnabled && !spreadView.active
1034 enabled: priv.sideStageEnabled && !spreadView.active
975 property var dragObject: null
1035 property var dragObject: null
976− property string appId: ""
1036+
1037+ property Item spreadDelegate
1038+
977 dragComponent: dragComponent
1039 dragComponent: dragComponent
978− dragComponentProperties: { "appId": appId }
1040+ dragComponentProperties: { "spreadDelegate": spreadDelegate }
979
1041
980 onPressed: {
1042 onPressed: {
981− function matchDelegate(obj) { return String(obj.objectName).indexOf("tabletSpreadDelegate") >= 0; }
1043+ function matchDelegate(obj) { return String(obj.objectName).indexOf("spreadDelegate") >= 0; }
982
1044
983 var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
1045 var delegateAtCenter = Functions.itemAt(spreadRow, x, y, matchDelegate);
984 if (!delegateAtCenter) return;
1046 if (!delegateAtCenter) return;
985
1047
986− appId = delegateAtCenter.appId;
1048+ spreadDelegate = delegateAtCenter;
987 }
1049 }
988
1050
989 onClicked: {
1051 onClicked: {
@@ -1003,11 +1065,11 @@ AbstractStage {
1003
1065
1004 Component {
1066 Component {
1005 id: dragComponent
1067 id: dragComponent
1006− SessionContainer {
1068+ SurfaceContainer {
1007− property string appId: ""
1069+ property Item spreadDelegate
1008− property var application: ApplicationManager.findApplication(appId)
1070+
1071+ surface: spreadDelegate ? spreadDelegate.surface : null
1009
1072
1010− session: application ? application.session : null
1011 interactive: false
1073 interactive: false
1012 resizeSurface: false
1074 resizeSurface: false
1013 focus: false
1075 focus: false
@@ -1019,10 +1081,11 @@ AbstractStage {
1019 Drag.hotSpot.y: height/2
1081 Drag.hotSpot.y: height/2
1020 // only accept opposite stage.
1082 // only accept opposite stage.
1021 Drag.keys: {
1083 Drag.keys: {
1022− if (!application) return "Disabled";
1084+ if (!surface) return "Disabled";
1023
1085
1024− if (application.stage === ApplicationInfo.MainStage) {
1086+ if (spreadDelegate.stage === ApplicationInfo.MainStage) {
1087+ if (spreadDelegate.application.supportedOrientations
1025− if (application.supportedOrientations & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1088+ & (Qt.PortraitOrientation|Qt.InvertedPortraitOrientation)) {
1026 return "MainStage";
1089 return "MainStage";
1027 }
1090 }
1028 return "Disabled";
1091 return "Disabled";
qml/Stages/TopLevelSurfaceRepeater.qmlnew file+52
@@ -0,0 +1,52 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+
19+Repeater {
20+ id: root
21+ // FIXME: This is a hack around us not knowing whether the Repeater has finished creating its
22+ // delegates on start up.
23+ // This is a problem when the stage gets a TopLevelSurfaceList already populated with several
24+ // rows.
25+ property bool startingUp: true
26+ onStartingUpChanged: {
27+ if (!startingUp) {
28+ // the top-most surface must be the focused one.
29+ var topmostDelegate = itemAt(0);
30+ topmostDelegate.focus = true;
31+ }
32+ }
33+
34+ onItemAdded: {
35+ if (startingUp) {
36+ checkIfStillStartingUp();
37+ }
38+ }
39+
40+ function checkIfStillStartingUp() {
41+ var i = 0;
42+ var missingDelegate = false;
43+ for (i = 0; i < model.count && !missingDelegate; ++i) {
44+ if (!itemAt(i)) {
45+ missingDelegate = true;
46+ }
47+ }
48+ if (!missingDelegate) {
49+ startingUp = false;
50+ }
51+ }
52+}
qml/Stages/TransformedSpreadDelegate.qml+4−4
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014,2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -312,7 +312,7 @@ SpreadDelegate {
312 // non-fullscreen window when they're stacked on top of each other on the
312 // non-fullscreen window when they're stacked on top of each other on the
313 // far left of the spread.
313 // far left of the spread.
314 Translate {
314 Translate {
315− y: !fullscreen && appWindowRotation === 180
315+ y: !root.fullscreen && appWindowRotation === 180
316 ? priv.topMarginProgress * maximizedAppTopMargin
316 ? priv.topMarginProgress * maximizedAppTopMargin
317 : 0
317 : 0
318 },
318 },
@@ -326,7 +326,7 @@ SpreadDelegate {
326 switch (appWindowRotation) {
326 switch (appWindowRotation) {
327 case 90:
327 case 90:
328 case 270:
328 case 270:
329− if (fullscreen) {
329+ if (root.fullscreen) {
330 return 1;
330 return 1;
331 } else {
331 } else {
332 return 1 + priv.topMarginProgress * maximizedAppTopMargin / spreadView.width;
332 return 1 + priv.topMarginProgress * maximizedAppTopMargin / spreadView.width;
@@ -340,7 +340,7 @@ SpreadDelegate {
340 yScale: {
340 yScale: {
341 switch (appWindowRotation) {
341 switch (appWindowRotation) {
342 case 0:
342 case 0:
343− if (fullscreen) {
343+ if (root.fullscreen) {
344 return 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height;
344 return 1 - priv.topMarginProgress * maximizedAppTopMargin / spreadView.height;
345 } else {
345 } else {
346 return 1;
346 return 1;
qml/Stages/TransformedTabletSpreadDelegate.qml+29−23
@@ -31,7 +31,6 @@ SpreadDelegate {
31 // Set this to true when this tile a currently active on either the MS or the SS.
31 // Set this to true when this tile a currently active on either the MS or the SS.
32 property bool active: false
32 property bool active: false
33
33
34− property int stage
35 property int zIndex
34 property int zIndex
36 property real progress: 0
35 property real progress: 0
37 property real animatedProgress: 0
36 property real animatedProgress: 0
@@ -48,13 +47,15 @@ SpreadDelegate {
48
47
49 property bool isInSideStage: false
48 property bool isInSideStage: false
50
49
50+ property int stage: ApplicationInfoInterface.MainStage
51+
51 property int dragOffset: 0
52 property int dragOffset: 0
52 readonly property alias xTranslateAnimating: xTranslateAnimation.running
53 readonly property alias xTranslateAnimating: xTranslateAnimation.running
53 readonly property bool offScreen: priv.xTranslate >= 0
54 readonly property bool offScreen: priv.xTranslate >= 0
54
55
55 dropShadow: spreadView.active ||
56 dropShadow: spreadView.active ||
56 (active
57 (active
57− && (stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
58+ && (root.stage == ApplicationInfoInterface.MainStage || !priv.shellIsLandscape)
58 && priv.xTranslate != 0)
59 && priv.xTranslate != 0)
59
60
60 onSelectedChanged: {
61 onSelectedChanged: {
@@ -90,7 +91,7 @@ SpreadDelegate {
90 id: priv
91 id: priv
91
92
92 // true if this is the next tile on the stack that comes in when dragging from the right
93 // 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
94 // true if the next tile in the stack is the nextInStack one. This one will be moved a bit to the left
95 // true if the next tile in the stack is the nextInStack one. This one will be moved a bit to the left
95 property bool movedActive: spreadView.nextZInStack == zIndex + 1
96 property bool movedActive: spreadView.nextZInStack == zIndex + 1
96 property real animatedEndDistance: linearAnimation(0, 2, root.endDistance, 0, root.progress)
97 property real animatedEndDistance: linearAnimation(0, 2, root.endDistance, 0, root.progress)
@@ -142,7 +143,7 @@ SpreadDelegate {
142 Behavior on xTranslate {
143 Behavior on xTranslate {
143 enabled: !spreadView.active &&
144 enabled: !spreadView.active &&
144 !snapAnimation.running &&
145 !snapAnimation.running &&
145− model.appId !== "unity8-dash" &&
146+ root.application.appId !== "unity8-dash" &&
146 spreadView.animateX &&
147 spreadView.animateX &&
147 !spreadView.beingResized &&
148 !spreadView.beingResized &&
148 priv.state !== "sideStage"
149 priv.state !== "sideStage"
@@ -156,8 +157,9 @@ SpreadDelegate {
156 var newTranslate = 0;
157 var newTranslate = 0;
157
158
158 // selected app or opposite stage active app.
159 // selected app or opposite stage active app.
159− if (isSelected || (otherSelected && root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage)) {
160+ if (isSelected || (otherSelected && root.active && spreadView.selectedDelegate
161+ && spreadView.selectedDelegate.stage !== root.stage)) {
160− if (stage == ApplicationInfoInterface.MainStage) {
162+ if (root.stage == ApplicationInfoInterface.MainStage) {
161 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
163 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.width, root.progress);
162 } else {
164 } else {
163 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
165 return linearAnimation(selectedProgress, negativeProgress, selectedXTranslate, -spreadView.sideStageWidth, root.progress);
@@ -168,8 +170,8 @@ SpreadDelegate {
168
170
169 // 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
171 // 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
170 // when we're only dragging the side stage in on top of a main stage app
172 // 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 &&
173+ var shouldMoveAway = spreadView.nextInStack >= 0 && priv.movedActive && root.stage === ApplicationInfoInterface.MainStage &&
172− ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
174+ topLevelSurfaceList.applicationAt(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage;
173
175
174 if (active) {
176 if (active) {
175 newTranslate -= root.width
177 newTranslate -= root.width
@@ -178,7 +180,7 @@ SpreadDelegate {
178 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
180 newTranslate += linearAnimation(0, spreadView.positionMarker2, 0, -units.gu(4), root.animatedProgress);
179 }
181 }
180 newTranslate += root.dragOffset;
182 newTranslate += root.dragOffset;
181− } else if (!spreadView.active && model.appId == "unity8-dash") {
183+ } else if (!spreadView.active && root.application.appId == "unity8-dash") {
182 newTranslate -= root.width;
184 newTranslate -= root.width;
183 }
185 }
184
186
@@ -190,14 +192,14 @@ SpreadDelegate {
190
192
191 if (spreadView.phase == 0) {
193 if (spreadView.phase == 0) {
192 if (nextInStack) {
194 if (nextInStack) {
193− if (stage == ApplicationInfoInterface.MainStage) {
195+ if (root.stage == ApplicationInfoInterface.MainStage) {
194 if (spreadView.sideStageVisible && root.progress > 0) {
196 if (spreadView.sideStageVisible && root.progress > 0) {
195 // Move it so it appears from behind the side stage immediately
197 // Move it so it appears from behind the side stage immediately
196 newTranslate += -spreadView.sideStageWidth;
198 newTranslate += -spreadView.sideStageWidth;
197 }
199 }
198 }
200 }
199
201
200− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
202+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
201 // This is when we only drag the side stage in, without rotation or snapping
203 // This is when we only drag the side stage in, without rotation or snapping
202 newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
204 newTranslate = linearAnimation(0, spreadView.positionMarker2, 0, -spreadView.sideStageWidth, root.animatedProgress);
203 } else {
205 } else {
@@ -210,7 +212,7 @@ SpreadDelegate {
210
212
211 if (spreadView.phase == 1) {
213 if (spreadView.phase == 1) {
212 if (nextInStack) {
214 if (nextInStack) {
213− if (stage == ApplicationInfoInterface.MainStage) {
215+ if (root.stage == ApplicationInfoInterface.MainStage) {
214 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
216 var startValue = -spreadView.sideStageWidth * spreadView.snapPosition + (spreadView.sideStageVisible ? -spreadView.sideStageWidth : 0);
215 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
217 newTranslate += linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startValue, priv.phase2StartTranslate, root.animatedProgress);
216 } else {
218 } else {
@@ -250,7 +252,8 @@ SpreadDelegate {
250 }
252 }
251
253
252 // selected app or opposite stage active app.
254 // 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)) {
254 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
257 return linearAnimation(selectedProgress, negativeProgress, selectedScale, 1, root.progress);
255 } else if (otherSelected) {
258 } else if (otherSelected) {
256 return selectedScale;
259 return selectedScale;
@@ -258,7 +261,7 @@ SpreadDelegate {
258
261
259 if (spreadView.phase == 0) {
262 if (spreadView.phase == 0) {
260 if (nextInStack) {
263 if (nextInStack) {
261− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
264+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
262 return 1;
265 return 1;
263 } else {
266 } else {
264 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
267 var targetScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
@@ -272,7 +275,7 @@ SpreadDelegate {
272 if (spreadView.phase == 1) {
275 if (spreadView.phase == 1) {
273 if (nextInStack) {
276 if (nextInStack) {
274 var startScale = 1;
277 var startScale = 1;
275− if (stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
278+ if (root.stage !== ApplicationInfoInterface.SideStage || spreadView.sideStageVisible) {
276 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
279 startScale = root.dragStartScale - ((root.dragStartScale - 1) * spreadView.snapPosition);
277 }
280 }
278 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
281 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, startScale, priv.phase2StartScale, root.animatedProgress);
@@ -295,7 +298,8 @@ SpreadDelegate {
295 }
298 }
296
299
297 // selected app or opposite stage active app.
300 // 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)) {
299 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
303 return linearAnimation(selectedProgress, negativeProgress, selectedAngle, 0, root.progress);
300 } else if (otherSelected) {
304 } else if (otherSelected) {
301 return selectedAngle;
305 return selectedAngle;
@@ -304,12 +308,12 @@ SpreadDelegate {
304 // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
308 // The tile should rotate a bit when another one comes on top, but not when only dragging the side stage in
305 var shouldMoveAway = spreadView.nextInStack == -1 ||
309 var shouldMoveAway = spreadView.nextInStack == -1 ||
306 spreadView.nextInStack >= 0 && priv.movedActive &&
310 spreadView.nextInStack >= 0 && priv.movedActive &&
307− (ApplicationManager.get(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
311+ (topLevelSurfaceList.applicationAt(spreadView.nextInStack).stage === ApplicationInfoInterface.MainStage ||
308− stage == ApplicationInfoInterface.SideStage);
312+ root.stage == ApplicationInfoInterface.SideStage);
309
313
310 if (spreadView.phase == 0) {
314 if (spreadView.phase == 0) {
311 if (nextInStack) {
315 if (nextInStack) {
312− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
316+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
313 return 0;
317 return 0;
314 } else {
318 } else {
315 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
319 return linearAnimation(0, spreadView.positionMarker2, root.startAngle, root.startAngle * (1-spreadView.snapPosition), root.animatedProgress);
@@ -321,7 +325,7 @@ SpreadDelegate {
321 }
325 }
322 if (spreadView.phase == 1) {
326 if (spreadView.phase == 1) {
323 if (nextInStack) {
327 if (nextInStack) {
324− if (stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
328+ if (root.stage == ApplicationInfoInterface.SideStage && !spreadView.sideStageVisible) {
325 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
329 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, 0, priv.phase2StartAngle, root.animatedProgress);
326 } else {
330 } else {
327 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
331 return linearAnimation(spreadView.positionMarker2, spreadView.positionMarker4, root.startAngle * (1-spreadView.snapPosition), priv.phase2StartAngle, root.animatedProgress);
@@ -345,7 +349,8 @@ SpreadDelegate {
345 if (root.isSelected) return 1;
349 if (root.isSelected) return 1;
346
350
347 if (otherSelected) {
351 if (otherSelected) {
348− if (root.active && spreadView.selectedApplication && spreadView.selectedApplication.stage !== stage) {
352+ if (root.active && spreadView.selectedDelegate
353+ && spreadView.selectedDelegate.stage !== root.stage) {
349 return 1;
354 return 1;
350 }
355 }
351 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
356 return linearAnimation(selectedProgress, negativeProgress, selectedOpacity, 0, root.progress);
@@ -355,7 +360,8 @@ SpreadDelegate {
355
360
356 property real topMarginProgress: {
361 property real topMarginProgress: {
357 // selected app or opposite stage active app.
362 // 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)) {
359 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
365 return linearAnimation(selectedProgress, negativeProgress, selectedTopMarginProgress, 0, root.progress);
360 }
366 }
361
367
@@ -379,7 +385,7 @@ SpreadDelegate {
379 when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
385 when: root.isInSideStage && spreadView.shiftedContentX == 0 && spreadView.phase == 0
380 PropertyChanges {
386 PropertyChanges {
381 target: priv;
387 target: priv;
382− xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-spreadView.sideStageDragProgress)
388+ xTranslate: -spreadView.sideStageWidth + spreadView.sideStageWidth * (1-sideStage.progress)
383 }
389 }
384 }
390 }
385 ]
391 ]
qml/Stages/WindowedFullscreenPolicy.qml+5−7
@@ -23,19 +23,17 @@ import Unity.Application 0.1
23// state of the window is returned to restored.
23// state of the window is returned to restored.
24QtObject {
24QtObject {
25 property bool active: true
25 property bool active: true
26− property QtObject application: null
26+ property QtObject surface: null
27
27
28− readonly property var lastSurface: application && application.session ?
29− application.session.lastSurface : null
30 property bool _firstTimeSurface: true
28 property bool _firstTimeSurface: true
31
29
32− onLastSurfaceChanged: {
30+ onSurfaceChanged: {
33− if (!active || !lastSurface) return;
31+ if (!active || !surface) return;
34 if (!_firstTimeSurface) return;
32 if (!_firstTimeSurface) return;
35 _firstTimeSurface = false;
33 _firstTimeSurface = false;
36
34
37− if (lastSurface.state === Mir.FullscreenState && lastSurface.shellChrome === Mir.LowChrome) {
35+ if (surface.state === Mir.FullscreenState && surface.shellChrome === Mir.LowChrome) {
38− lastSurface.state = Mir.RestoredState;
36+ surface.state = Mir.RestoredState;
39 }
37 }
40 }
38 }
41}
39}
qml/Tutorial/TutorialBottom.qml+1−1
@@ -47,7 +47,7 @@ TutorialPage {
47 readonly property real sideStageWidth: root.usageScenario === "tablet" && stage.sideStageVisible ?
47 readonly property real sideStageWidth: root.usageScenario === "tablet" && stage.sideStageVisible ?
48 stage.sideStageWidth : 0
48 stage.sideStageWidth : 0
49 readonly property bool isMainStageApp: usageScenario !== "tablet" ||
49 readonly property bool isMainStageApp: usageScenario !== "tablet" ||
50− application.stage === ApplicationInfoInterface.MainStage
50+ stage.stageFocusedSurface === ApplicationInfoInterface.MainStage
51 readonly property real dragAreaHeight: units.gu(3) // based on PageWithBottomEdge.qml
51 readonly property real dragAreaHeight: units.gu(3) // based on PageWithBottomEdge.qml
52 readonly property real targetDistance: height * 0.2 + dragAreaHeight // based on PageWithBottomEdge.qml
52 readonly property real targetDistance: height * 0.2 + dragAreaHeight // based on PageWithBottomEdge.qml
53
53
tests/mocks/Unity/Application/ApplicationInfo.cpp+181−108
@@ -16,8 +16,7 @@
16
16
17#include "ApplicationInfo.h"
17#include "ApplicationInfo.h"
18#include "MirSurface.h"
18#include "MirSurface.h"
19−#include "Session.h"
19+#include "SurfaceManager.h"
20−#include "SessionManager.h"
21
20
22#include <paths.h>
21#include <paths.h>
23
22
@@ -25,97 +24,114 @@
25#include <QQuickItem>
24#include <QQuickItem>
26#include <QQuickView>
25#include <QQuickView>
27#include <QQmlComponent>
26#include <QQmlComponent>
28−#include <QTimer>
27+
28+#define APPLICATION_DEBUG 0
29+
30+#if APPLICATION_DEBUG
31+#define DEBUG_MSG(params) qDebug().nospace() << "Application["<<appId()<<"]::" << __func__ << " " << params
32+
33+QString stateToStr(ApplicationInfo::State state)
34+{
35+ switch (state) {
36+ case ApplicationInfo::Starting:
37+ return "starting";
38+ case ApplicationInfo::Running:
39+ return "running";
40+ case ApplicationInfo::Suspended:
41+ return "suspended";
42+ case ApplicationInfo::Stopped:
43+ return "stopped";
44+ default:
45+ return "???";
46+ };
47+}
48+
49+#else
50+#define DEBUG_MSG(params) ((void)0)
51+#endif
52+
53+#define WARNING_MSG(params) qWarning().nospace() << "Application["<<appId()<<"]::" << __func__ << " " << params
29
54
30ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
55ApplicationInfo::ApplicationInfo(const QString &appId, QObject *parent)
31 : ApplicationInfoInterface(appId, parent)
56 : ApplicationInfoInterface(appId, parent)
32 , m_appId(appId)
57 , 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)
48{
58{
59+ connect(&m_surfaceList, &MirSurfaceListModel::countChanged,
60+ this, &ApplicationInfo::onSurfaceCountChanged, Qt::QueuedConnection);
61+
62+ m_surfaceCreationTimer.setSingleShot(true);
63+ m_surfaceCreationTimer.setInterval(500);
64+ connect(&m_surfaceCreationTimer, &QTimer::timeout, this, &ApplicationInfo::createSurface);
49}
65}
50
66
51ApplicationInfo::ApplicationInfo(QObject *parent)
67ApplicationInfo::ApplicationInfo(QObject *parent)
52− : ApplicationInfoInterface(QString(), parent)
68+ : ApplicationInfo(QString(), parent)
53− , m_stage(MainStage)
54− , m_state(Stopped)
55− , m_focused(false)
56− , m_fullscreen(false)
57− , m_session(0)
58− , m_supportedOrientations(Qt::PortraitOrientation |
59− Qt::LandscapeOrientation |
60− Qt::InvertedPortraitOrientation |
61− Qt::InvertedLandscapeOrientation)
62− , m_rotatesWindowContents(false)
63− , m_requestedState(RequestedRunning)
64− , m_isTouchApp(true)
65− , m_exemptFromLifecycle(false)
66− , m_manualSurfaceCreation(false)
67− , m_shellChrome(Mir::NormalChrome)
68{
69{
69}
70}
70
71
71ApplicationInfo::~ApplicationInfo()
72ApplicationInfo::~ApplicationInfo()
72{
73{
73− delete m_session;
74}
74}
75
75
76−void ApplicationInfo::createSession()
76+void ApplicationInfo::createSurface()
77{
77{
78− if (m_session || state() == ApplicationInfo::Stopped) { return; }
78+ if (state() == ApplicationInfo::Stopped) { return; }
79−
80− setSession(SessionManager::singleton()->createSession(appId(), m_screenshotFileName));
81−}
82
79
83−void ApplicationInfo::destroySession()
80+ QString surfaceName = name();
84−{
81+ if (m_surfaceList.count() > 0) {
85− Session *session = this->session();
82+ surfaceName.append(QString(" %1").arg(m_surfaceList.count()+1));
86− setSession(nullptr);
87− delete session;
88−}
83+ }
89
84
90−void ApplicationInfo::setSession(Session* session)
85+ auto surfaceManager = SurfaceManager::instance();
91−{
86+ if (!surfaceManager) {
92− if (m_session == session)
87+ WARNING_MSG("No SurfaceManager");
93 return;
88 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();
100 }
89 }
101
90
102− m_session = session;
91+ auto surface = surfaceManager->createSurface(surfaceName,
103−
92+ Mir::NormalType,
104− if (m_session) {
93+ fullscreen() ? Mir::FullscreenState : Mir::MaximizedState,
105− m_session->setApplication(this);
94+ m_screenshotFileName);
106− m_session->setParent(this);
95+
107− m_session->setFullscreen(m_fullscreen);
96+ surface->setShellChrome(m_shellChrome);
108− SessionManager::singleton()->registerSession(m_session);
97+
109− connect(m_session, &Session::surfaceAdded,
98+ m_surfaceList.appendSurface(surface);
110− this, &ApplicationInfo::onSessionSurfaceAdded);
99+
111− connect(m_session, &Session::fullscreenChanged, this, &ApplicationInfo::fullscreenChanged);
100+ ++m_liveSurfaceCount;
101+ connect(surface, &MirSurface::liveChanged, this, [this, surface](){
102+ if (!surface->live()) {
103+ --m_liveSurfaceCount;
104+ if (m_liveSurfaceCount == 0) {
105+ if (m_closingSurfaces.contains(surface)
106+ || (m_state == Running && m_requestedState == RequestedRunning)) {
107+ Q_EMIT closed();
108+ }
109+ setState(Stopped);
110+ } else {
111+ if (m_closingSurfaces.contains(surface) && m_requestedState == RequestedSuspended
112+ && m_closingSurfaces.count() == 1) {
113+ setState(Suspended);
114+ }
115+ }
116+ m_closingSurfaces.removeAll(surface);
117+ }
118+ });
119+ connect(surface, &MirSurface::closeRequested, this, [this, surface](){
120+ m_closingSurfaces.append(surface);
121+ if (m_state == Suspended) {
122+ // resume to allow application to close its surface
123+ setState(Running);
124+ }
125+ });
126+ connect(surface, &MirSurface::focusRequested, this, &ApplicationInfo::focusRequested);
112
127
113− if (!m_manualSurfaceCreation) {
128+ if (m_state == Starting) {
114− QTimer::singleShot(500, m_session, &Session::createSurface);
129+ if (m_requestedState == RequestedRunning) {
130+ setState(Running);
131+ } else {
132+ setState(Suspended);
115 }
133 }
116 }
134 }
117−
118− Q_EMIT sessionChanged(m_session);
119}
135}
120
136
121void ApplicationInfo::setIconId(const QString &iconId)
137void ApplicationInfo::setIconId(const QString &iconId)
@@ -138,10 +154,6 @@ void ApplicationInfo::setScreenshotId(const QString &screenshotId)
138
154
139 if (screenshotFileName != m_screenshotFileName) {
155 if (screenshotFileName != m_screenshotFileName) {
140 m_screenshotFileName = screenshotFileName;
156 m_screenshotFileName = screenshotFileName;
141−
142− if (m_session) {
143− m_session->setScreenshot(screenshotFileName);
144− }
145 }
157 }
146}
158}
147
159
@@ -172,38 +184,53 @@ void ApplicationInfo::setStage(Stage value)
172void ApplicationInfo::setState(State value)
184void ApplicationInfo::setState(State value)
173{
185{
174 if (value != m_state) {
186 if (value != m_state) {
187+ DEBUG_MSG(qPrintable(stateToStr(value)));
188+ if (!m_manualSurfaceCreation && value == ApplicationInfo::Starting) {
189+ Q_ASSERT(m_surfaceList.count() == 0);
190+ m_surfaceCreationTimer.start();
191+ } else if (value == ApplicationInfo::Stopped) {
192+ m_surfaceCreationTimer.stop();
193+ for (int i = 0; i < m_surfaceList.count(); ++i) {
194+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
195+ surface->setLive(false);
196+ }
197+ }
198+
175 m_state = value;
199 m_state = value;
176 Q_EMIT stateChanged(value);
200 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− }
185 }
201 }
186}
202}
187
203
188−void ApplicationInfo::setFocused(bool value)
204+void ApplicationInfo::close()
189{
205{
190− if (value != m_focused) {
206+ DEBUG_MSG("");
191− m_focused = value;
207+
192− Q_EMIT focusedChanged(value);
208+ if (m_surfaceList.count() > 0) {
209+ for (int i = 0; i < m_surfaceList.count(); ++i) {
210+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
211+ surface->close();
212+ }
213+ } else {
214+ setState(Stopped);
215+ Q_EMIT closed();
193 }
216 }
194}
217}
195
218
196void ApplicationInfo::setFullscreen(bool value)
219void ApplicationInfo::setFullscreen(bool value)
197{
220{
198 m_fullscreen = value;
221 m_fullscreen = value;
199− if (m_session) {
222+ if (m_surfaceList.rowCount() > 0) {
200− m_session->setFullscreen(value);
223+ m_surfaceList.get(0)->setState(Mir::FullscreenState);
201 }
224 }
202}
225}
203
226
204bool ApplicationInfo::fullscreen() const
227bool ApplicationInfo::fullscreen() const
205{
228{
206− return m_session ? m_session->fullscreen() : m_fullscreen;
229+ if (m_surfaceList.rowCount() > 0) {
230+ return m_surfaceList.get(0)->state() == Mir::FullscreenState;
231+ } else {
232+ return m_fullscreen;
233+ }
207}
234}
208
235
209void ApplicationInfo::setManualSurfaceCreation(bool value)
236void ApplicationInfo::setManualSurfaceCreation(bool value)
@@ -241,15 +268,28 @@ ApplicationInfo::RequestedState ApplicationInfo::requestedState() const
241
268
242void ApplicationInfo::setRequestedState(RequestedState value)
269void ApplicationInfo::setRequestedState(RequestedState value)
243{
270{
244− if (m_requestedState != value) {
271+ if (m_requestedState == value) {
245− m_requestedState = value;
272+ return;
246− Q_EMIT requestedStateChanged(m_requestedState);
273+ }
274+ DEBUG_MSG((value == RequestedRunning ? "RequestedRunning" : "RequestedSuspended") );
247
275
276+ m_requestedState = value;
277+ Q_EMIT requestedStateChanged(m_requestedState);
278+
248− if (m_requestedState == RequestedRunning && m_state == Suspended) {
279+ if (m_requestedState == RequestedRunning) {
280+
281+ if (m_state == Suspended) {
282+ Q_ASSERT(m_liveSurfaceCount > 0);
249 setState(Running);
283 setState(Running);
250− } else if (m_requestedState == RequestedSuspended && m_state == Running) {
284+ } else if (m_state == Stopped) {
285+ Q_ASSERT(m_liveSurfaceCount == 0);
286+ // it's restarting
251− setState(Suspended);
287+ setState(Starting);
252 }
288 }
289+
290+ } else if (m_requestedState == RequestedSuspended && m_state == Running
291+ && m_closingSurfaces.isEmpty()) {
292+ setState(Suspended);
253 }
293 }
254}
294}
255
295
@@ -263,18 +303,6 @@ void ApplicationInfo::setIsTouchApp(bool isTouchApp)
263 m_isTouchApp = isTouchApp;
303 m_isTouchApp = isTouchApp;
264}
304}
265
305
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−
278bool ApplicationInfo::exemptFromLifecycle() const
306bool ApplicationInfo::exemptFromLifecycle() const
279{
307{
280 return m_exemptFromLifecycle;
308 return m_exemptFromLifecycle;
@@ -305,7 +333,52 @@ void ApplicationInfo::setInitialSurfaceSize(const QSize &size)
305void ApplicationInfo::setShellChrome(Mir::ShellChrome shellChrome)
333void ApplicationInfo::setShellChrome(Mir::ShellChrome shellChrome)
306{
334{
307 m_shellChrome = shellChrome;
335 m_shellChrome = shellChrome;
308− if (m_session && m_session->lastSurface()) {
336+ if (m_surfaceList.rowCount() > 0) {
309− m_session->lastSurface()->setShellChrome(shellChrome);
337+ static_cast<MirSurface*>(m_surfaceList.get(0))->setShellChrome(shellChrome);
338+ }
339+}
340+
341+bool ApplicationInfo::focused() const
342+{
343+ bool someSurfaceHasFocus = false; // to be proven wrong
344+ for (int i = 0; i < m_surfaceList.count() && !someSurfaceHasFocus; ++i) {
345+ someSurfaceHasFocus = m_surfaceList.get(i)->focused();
346+ }
347+ return someSurfaceHasFocus;
348+}
349+
350+void ApplicationInfo::setFocused(bool value)
351+{
352+ if (focused() == value) {
353+ return;
354+ }
355+
356+ if (value) {
357+ if (m_surfaceList.count() > 0) {
358+ m_surfaceList.get(0)->requestFocus();
359+ }
360+ } else {
361+ for (int i = 0; i < m_surfaceList.count(); ++i) {
362+ MirSurface *surface = static_cast<MirSurface*>(m_surfaceList.get(i));
363+ if (surface->focused()) {
364+ surface->setFocused(false);
365+ }
366+ }
367+ }
368+}
369+
370+void ApplicationInfo::onSurfaceCountChanged()
371+{
372+ if (m_surfaceList.count() == 0 && m_state == Running) {
373+ setState(Stopped);
374+ }
375+}
376+
377+void ApplicationInfo::requestFocus()
378+{
379+ if (m_surfaceList.count() == 0) {
380+ Q_EMIT focusRequested();
381+ } else {
382+ m_surfaceList.get(0)->requestFocus();
310 }
383 }
311}
384}
tests/mocks/Unity/Application/ApplicationInfo.h+39−24
@@ -20,25 +20,32 @@
20#include <QObject>
20#include <QObject>
21
21
22class MirSurface;
22class MirSurface;
23−class Session;
24
23
25// unity-api
24// unity-api
26#include <unity/shell/application/ApplicationInfoInterface.h>
25#include <unity/shell/application/ApplicationInfoInterface.h>
27#include <unity/shell/application/Mir.h>
26#include <unity/shell/application/Mir.h>
28
27
28+#include "MirSurfaceListModel.h"
29+
30+#include <QList>
31+#include <QTimer>
32+
29using namespace unity::shell::application;
33using namespace unity::shell::application;
30
34
31class ApplicationInfo : public ApplicationInfoInterface {
35class ApplicationInfo : public ApplicationInfoInterface {
32 Q_OBJECT
36 Q_OBJECT
33
37
38+ ////
39+ // FIXME: Remove those
34 Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged)
40 Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged)
35− Q_PROPERTY(Session* session READ session NOTIFY sessionChanged)
36
41
37 // Only exists in this fake implementation
42 // Only exists in this fake implementation
38
43
39 // whether the test code will explicitly control the creation of the application surface
44 // whether the test code will explicitly control the creation of the application surface
40 Q_PROPERTY(bool manualSurfaceCreation READ manualSurfaceCreation WRITE setManualSurfaceCreation NOTIFY manualSurfaceCreationChanged)
45 Q_PROPERTY(bool manualSurfaceCreation READ manualSurfaceCreation WRITE setManualSurfaceCreation NOTIFY manualSurfaceCreationChanged)
41
46
47+ Q_PROPERTY(QString screenshot READ screenshot CONSTANT)
48+
42public:
49public:
43 ApplicationInfo(QObject *parent = nullptr);
50 ApplicationInfo(QObject *parent = nullptr);
44 ApplicationInfo(const QString &appId, QObject *parent = nullptr);
51 ApplicationInfo(const QString &appId, QObject *parent = nullptr);
@@ -66,8 +73,7 @@ public:
66 Q_INVOKABLE void setState(State value);
73 Q_INVOKABLE void setState(State value);
67 State state() const override { return m_state; }
74 State state() const override { return m_state; }
68
75
69− void setFocused(bool value);
70− bool focused() const override { return m_focused; }
76+ bool focused() const override;
71
77
72 QString splashTitle() const override { return QString(); }
78 QString splashTitle() const override { return QString(); }
73 QUrl splashImage() const override { return QUrl(); }
79 QUrl splashImage() const override { return QUrl(); }
@@ -100,21 +106,26 @@ public:
100 void setInitialSurfaceSize(const QSize &size) override;
106 void setInitialSurfaceSize(const QSize &size) override;
101
107
102 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
108 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
103−public:
109+
104− void setSession(Session* session);
110+ MirSurfaceListInterface* surfaceList() override { return &m_surfaceList; }
105− Session* session() const { return m_session; }
111+
112+ void setFocused(bool value);
113+
114+ //////
115+ // internal mock stuff
116+ void close();
117+ void requestFocus();
106
118
107Q_SIGNALS:
119Q_SIGNALS:
108− void sessionChanged(Session*);
109 void fullscreenChanged(bool value);
120 void fullscreenChanged(bool value);
110 void manualSurfaceCreationChanged(bool value);
121 void manualSurfaceCreationChanged(bool value);
122+ void closed();
111
123
112public Q_SLOTS:
124public Q_SLOTS:
113− Q_INVOKABLE void createSession();
125+ Q_INVOKABLE void createSurface();
114− Q_INVOKABLE void destroySession();
115
126
116private Q_SLOTS:
127private Q_SLOTS:
117− void onSessionSurfaceAdded(MirSurface*);
128+ void onSurfaceCountChanged();
118
129
119private:
130private:
120 void setIcon(const QUrl &value);
131 void setIcon(const QUrl &value);
@@ -124,20 +135,24 @@ private:
124 QString m_appId;
135 QString m_appId;
125 QString m_name;
136 QString m_name;
126 QUrl m_icon;
137 QUrl m_icon;
127− Stage m_stage;
138+ Stage m_stage{MainStage};
128− State m_state;
139+ State m_state{Stopped};
129− bool m_focused;
130− bool m_fullscreen;
140+ bool m_fullscreen{false};
131− Session* m_session;
132− Qt::ScreenOrientations m_supportedOrientations;
141+ Qt::ScreenOrientations m_supportedOrientations{Qt::PortraitOrientation |
142+ Qt::LandscapeOrientation |
143+ Qt::InvertedPortraitOrientation |
144+ Qt::InvertedLandscapeOrientation};
133− bool m_rotatesWindowContents;
145+ bool m_rotatesWindowContents{false};
134− RequestedState m_requestedState;
146+ RequestedState m_requestedState{RequestedRunning};
135− bool m_isTouchApp;
147+ bool m_isTouchApp{true};
136− bool m_exemptFromLifecycle;
148+ bool m_exemptFromLifecycle{false};
137 QSize m_initialSurfaceSize;
149 QSize m_initialSurfaceSize;
138−
150+ MirSurfaceListModel m_surfaceList;
151+ int m_liveSurfaceCount{0};
152+ QTimer m_surfaceCreationTimer;
153+ QList<MirSurface*> m_closingSurfaces;
139− bool m_manualSurfaceCreation;
154+ bool m_manualSurfaceCreation{false};
140− Mir::ShellChrome m_shellChrome;
155+ Mir::ShellChrome m_shellChrome{Mir::NormalChrome};
141};
156};
142
157
143Q_DECLARE_METATYPE(ApplicationInfo*)
158Q_DECLARE_METATYPE(ApplicationInfo*)
tests/mocks/Unity/Application/ApplicationManager.cpp+109−74
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013-2015 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,7 @@
16
16
17#include "ApplicationManager.h"
17#include "ApplicationManager.h"
18#include "ApplicationInfo.h"
18#include "ApplicationInfo.h"
19−#include "Session.h"
19+#include "MirSurface.h"
20−#include "ApplicationTestInterface.h"
21
20
22#include <paths.h>
21#include <paths.h>
23#include <csignal>
22#include <csignal>
@@ -31,28 +30,20 @@
31#include <QDateTime>
30#include <QDateTime>
32#include <QtDBus/QtDBus>
31#include <QtDBus/QtDBus>
33
32
34−ApplicationManager *ApplicationManager::the_application_manager = nullptr;
33+#define APPLICATIONMANAGER_DEBUG 0
35
34
36−ApplicationManager *ApplicationManager::singleton()
35+#if APPLICATIONMANAGER_DEBUG
37−{
36+#define DEBUG_MSG(params) qDebug().nospace() << "ApplicationManager::" << __func__ << " " << params
38− if (!the_application_manager) {
37+#else
39− the_application_manager = new ApplicationManager();
38+#define DEBUG_MSG(params) ((void)0)
39+#endif
40
40
41− // Emit signal to notify Upstart that Mir is ready to receive client connections
41+namespace unityapi = unity::shell::application;
42− // see http://upstart.ubuntu.com/cookbook/#expect-stop
43− if (qgetenv("UNITY_MIR_EMITS_SIGSTOP") == "1") {
44− raise(SIGSTOP);
45− }
46− }
47− return the_application_manager;
48−}
49
42
50ApplicationManager::ApplicationManager(QObject *parent)
43ApplicationManager::ApplicationManager(QObject *parent)
51 : ApplicationManagerInterface(parent)
44 : ApplicationManagerInterface(parent)
52{
45{
53− m_roleNames.insert(RoleSession, "session");
46+ DEBUG_MSG("");
54− m_roleNames.insert(RoleFullscreen, "fullscreen");
55−
56 buildListOfAvailableApplications();
47 buildListOfAvailableApplications();
57
48
58 // polling to find out when the toplevel window has been created as there's
49 // polling to find out when the toplevel window has been created as there's
@@ -61,6 +52,19 @@ ApplicationManager::ApplicationManager(QObject *parent)
61 this, &ApplicationManager::onWindowCreatedTimerTimeout);
52 this, &ApplicationManager::onWindowCreatedTimerTimeout);
62 m_windowCreatedTimer.setSingleShot(false);
53 m_windowCreatedTimer.setSingleShot(false);
63 m_windowCreatedTimer.start(200);
54 m_windowCreatedTimer.start(200);
55+
56+ Q_ASSERT(MirFocusController::instance());
57+ connect(MirFocusController::instance(), &MirFocusController::focusedSurfaceChanged,
58+ this, &ApplicationManager::updateFocusedApplication, Qt::QueuedConnection);
59+
60+
61+ // Emit signal to notify Upstart that Mir is ready to receive client connections
62+ // see http://upstart.ubuntu.com/cookbook/#expect-stop
63+ // We do this because some autopilot tests actually use this mock Unity.Application module,
64+ // so we have to mimic what the real ApplicationManager does in that regard.
65+ if (qgetenv("UNITY_MIR_EMITS_SIGSTOP") == "1") {
66+ raise(SIGSTOP);
67+ }
64}
68}
65
69
66ApplicationManager::~ApplicationManager()
70ApplicationManager::~ApplicationManager()
@@ -78,7 +82,6 @@ void ApplicationManager::onWindowCreatedTimerTimeout()
78void ApplicationManager::onWindowCreated()
82void ApplicationManager::onWindowCreated()
79{
83{
80 startApplication("unity8-dash");
84 startApplication("unity8-dash");
81− focusApplication("unity8-dash");
82}
85}
83
86
84int ApplicationManager::rowCount(const QModelIndex& parent) const {
87int ApplicationManager::rowCount(const QModelIndex& parent) const {
@@ -109,10 +112,8 @@ QVariant ApplicationManager::data(const QModelIndex& index, int role) const {
109 return app->isTouchApp();
112 return app->isTouchApp();
110 case RoleExemptFromLifecycle:
113 case RoleExemptFromLifecycle:
111 return app->exemptFromLifecycle();
114 return app->exemptFromLifecycle();
112− case RoleSession:
115+ case RoleApplication:
113− return QVariant::fromValue(app->session());
116+ return QVariant::fromValue(static_cast<unityapi::ApplicationInfoInterface*>(app));
114− case RoleFullscreen:
115− return app->fullscreen();
116 default:
117 default:
117 return QVariant();
118 return QVariant();
118 }
119 }
@@ -144,19 +145,17 @@ QModelIndex ApplicationManager::findIndex(ApplicationInfo* application)
144 return QModelIndex();
145 return QModelIndex();
145}
146}
146
147
147−void ApplicationManager::add(ApplicationInfo *application) {
148+bool ApplicationManager::add(ApplicationInfo *application) {
148 if (!application || m_runningApplications.contains(application)) {
149 if (!application || m_runningApplications.contains(application)) {
149− return;
150+ return false;
150 }
151 }
152+ DEBUG_MSG(application->appId());
153+
154+ application->setState(ApplicationInfo::Starting);
151
155
152 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
156 beginInsertRows(QModelIndex(), m_runningApplications.size(), m_runningApplications.size());
153 m_runningApplications.append(application);
157 m_runningApplications.append(application);
154
158
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− });
160 connect(application, &ApplicationInfo::focusedChanged, this, [application, this]() {
159 connect(application, &ApplicationInfo::focusedChanged, this, [application, this]() {
161 QModelIndex appIndex = findIndex(application);
160 QModelIndex appIndex = findIndex(application);
162 if (!appIndex.isValid()) return;
161 if (!appIndex.isValid()) return;
@@ -173,19 +172,27 @@ void ApplicationManager::add(ApplicationInfo *application) {
173 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleStage);
172 Q_EMIT dataChanged(appIndex, appIndex, QVector<int>() << ApplicationManager::RoleStage);
174 });
173 });
175
174
175+ connect(application, &ApplicationInfo::closed, this, [application, this]() {
176+ this->remove(application);
177+ });
178+ connect(application, &ApplicationInfo::focusRequested, this, [application, this]() {
179+ Q_EMIT this->focusRequested(application->appId());
180+ });
181+
176 endInsertRows();
182 endInsertRows();
177− Q_EMIT applicationAdded(application->appId());
178 Q_EMIT countChanged();
183 Q_EMIT countChanged();
179 if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
184 if (count() == 1) Q_EMIT emptyChanged(isEmpty()); // was empty but not anymore
185+
186+ return true;
180}
187}
181
188
182void ApplicationManager::remove(ApplicationInfo *application) {
189void ApplicationManager::remove(ApplicationInfo *application) {
183 int i = m_runningApplications.indexOf(application);
190 int i = m_runningApplications.indexOf(application);
184 if (i != -1) {
191 if (i != -1) {
192+ DEBUG_MSG(application->appId());
185 beginRemoveRows(QModelIndex(), i, i);
193 beginRemoveRows(QModelIndex(), i, i);
186 m_runningApplications.removeAt(i);
194 m_runningApplications.removeAt(i);
187 endRemoveRows();
195 endRemoveRows();
188− Q_EMIT applicationRemoved(application->appId());
189 Q_EMIT countChanged();
196 Q_EMIT countChanged();
190 if (isEmpty()) Q_EMIT emptyChanged(isEmpty());
197 if (isEmpty()) Q_EMIT emptyChanged(isEmpty());
191 }
198 }
@@ -209,18 +216,29 @@ void ApplicationManager::move(int from, int to) {
209ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
216ApplicationInfo* ApplicationManager::startApplication(const QString &appId,
210 const QStringList &arguments)
217 const QStringList &arguments)
211{
218{
219+ DEBUG_MSG(appId);
212 Q_UNUSED(arguments)
220 Q_UNUSED(arguments)
221+
213− ApplicationInfo *application = add(appId);
222+ ApplicationInfo *application = findApplication(appId);
223+ if (application) {
224+ // the requested app is already running
225+ return application;
226+ } else {
227+ application = add(appId);
228+ }
229+
230+ // most likely not among the available ones
214 if (!application)
231 if (!application)
215− return 0;
232+ return nullptr;
216− application->setState(ApplicationInfo::Starting);
233+
234+ Q_EMIT application->focusRequested(); // we assume that an application that's starting up wants focus
217
235
218 return application;
236 return application;
219}
237}
220
238
221ApplicationInfo* ApplicationManager::add(QString appId)
239ApplicationInfo* ApplicationManager::add(QString appId)
222{
240{
223− ApplicationInfo *application = 0;
241+ ApplicationInfo *application = nullptr;
224
242
225 for (ApplicationInfo *availableApp : m_availableApplications) {
243 for (ApplicationInfo *availableApp : m_availableApplications) {
226 if (availableApp->appId() == appId) {
244 if (availableApp->appId() == appId) {
@@ -229,23 +247,23 @@ ApplicationInfo* ApplicationManager::add(QString appId)
229 }
247 }
230 }
248 }
231
249
232− if (application)
250+ if (application) {
233− add(application);
251+ if (!add(application)) {
252+ application = nullptr;
253+ }
254+ }
234
255
235 return application;
256 return application;
236}
257}
237
258
238bool ApplicationManager::stopApplication(const QString &appId)
259bool ApplicationManager::stopApplication(const QString &appId)
239{
260{
261+ DEBUG_MSG(appId);
240 ApplicationInfo *application = findApplication(appId);
262 ApplicationInfo *application = findApplication(appId);
241 if (application == nullptr)
263 if (application == nullptr)
242 return false;
264 return false;
243
265
244− if (application->appId() == focusedApplicationId()) {
266+ application->close();
245− unfocusCurrentApplication();
246− }
247− application->setState(ApplicationInfo::Stopped);
248− remove(application);
249 return true;
267 return true;
250}
268}
251
269
@@ -258,44 +276,17 @@ QString ApplicationManager::focusedApplicationId() const {
258 return QString();
276 return QString();
259}
277}
260
278
261−bool ApplicationManager::focusApplication(const QString &appId)
279+bool ApplicationManager::requestFocusApplication(const QString &appId)
262{
280{
263 ApplicationInfo *application = findApplication(appId);
281 ApplicationInfo *application = findApplication(appId);
264 if (application == nullptr)
282 if (application == nullptr)
265 return false;
283 return false;
266
284
267− // unfocus currently focused app
268− for (ApplicationInfo *app : m_runningApplications) {
269− if (app->focused()) {
270− app->setFocused(false);
271− }
272− }
273−
274− // focus this app
275− application->setFocused(true);
285+ application->requestFocus();
276
286
277− // move app to top of stack
278− move(m_runningApplications.indexOf(application), 0);
279− Q_EMIT focusedApplicationIdChanged();
280 return true;
287 return true;
281}
288}
282
289
283−bool ApplicationManager::requestFocusApplication(const QString &appId)
284−{
285− QMetaObject::invokeMethod(this, "focusRequested", Qt::QueuedConnection, Q_ARG(QString, appId));
286− return true;
287−}
288−
289−void ApplicationManager::unfocusCurrentApplication()
290−{
291− for (ApplicationInfo *app : m_runningApplications) {
292− if (app->focused()) {
293− app->setFocused(false);
294− }
295− }
296− Q_EMIT focusedApplicationIdChanged();
297−}
298−
299void ApplicationManager::buildListOfAvailableApplications()
290void ApplicationManager::buildListOfAvailableApplications()
300{
291{
301 /*
292 /*
@@ -328,7 +319,6 @@ void ApplicationManager::buildListOfAvailableApplications()
328 application->setName("Camera");
319 application->setName("Camera");
329 application->setScreenshotId("camera");
320 application->setScreenshotId("camera");
330 application->setIconId("camera");
321 application->setIconId("camera");
331− application->setShellChrome(Mir::LowChrome);
332 application->setFullscreen(true);
322 application->setFullscreen(true);
333 application->setSupportedOrientations(Qt::PortraitOrientation
323 application->setSupportedOrientations(Qt::PortraitOrientation
334 | Qt::LandscapeOrientation
324 | Qt::LandscapeOrientation
@@ -473,3 +463,48 @@ bool ApplicationManager::isEmpty() const
473{
463{
474 return m_runningApplications.isEmpty();
464 return m_runningApplications.isEmpty();
475}
465}
466+
467+void ApplicationManager::updateFocusedApplication()
468+{
469+ ApplicationInfo *focusedApplication = nullptr;
470+ ApplicationInfo *previouslyFocusedApplication = nullptr;
471+
472+ auto controller = MirFocusController::instance();
473+ if (!controller) {
474+ return;
475+ }
476+
477+ MirSurface *surface = static_cast<MirSurface*>(controller->focusedSurface());
478+ if (surface) {
479+ focusedApplication = findApplication(surface);
480+ }
481+
482+ surface = static_cast<MirSurface*>(controller->previouslyFocusedSurface());
483+ if (surface) {
484+ previouslyFocusedApplication = findApplication(surface);
485+ }
486+
487+ if (focusedApplication != previouslyFocusedApplication) {
488+ if (focusedApplication) {
489+ DEBUG_MSG("focused " << focusedApplication->appId());
490+ Q_EMIT focusedApplication->focusedChanged(true);
491+ this->move(this->m_runningApplications.indexOf(focusedApplication), 0);
492+ }
493+ if (previouslyFocusedApplication) {
494+ DEBUG_MSG("unfocused " << previouslyFocusedApplication->appId());
495+ Q_EMIT previouslyFocusedApplication->focusedChanged(false);
496+ }
497+ Q_EMIT focusedApplicationIdChanged();
498+ }
499+}
500+
501+ApplicationInfo *ApplicationManager::findApplication(MirSurface* surface)
502+{
503+ for (ApplicationInfo *app : m_runningApplications) {
504+ auto surfaceList = static_cast<MirSurfaceListModel*>(app->surfaceList());
505+ if (surfaceList->contains(surface)) {
506+ return app;
507+ }
508+ }
509+ return nullptr;
510+}
tests/mocks/Unity/Application/ApplicationManager.h+12−14
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013-2014 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -26,12 +26,19 @@
26// unity-api
26// unity-api
27#include <unity/shell/application/ApplicationManagerInterface.h>
27#include <unity/shell/application/ApplicationManagerInterface.h>
28
28
29+namespace unity {
30+ namespace shell {
31+ namespace application {
32+ class MirSurfaceInterface;
33+ }
34+ }
35+}
36+
29class QQuickItem;
37class QQuickItem;
30using namespace unity::shell::application;
38using namespace unity::shell::application;
31
39
32class ApplicationManager : public ApplicationManagerInterface {
40class ApplicationManager : public ApplicationManagerInterface {
33 Q_OBJECT
41 Q_OBJECT
34− Q_FLAGS(ExecFlags)
35
42
36 Q_PROPERTY(bool empty READ isEmpty NOTIFY emptyChanged)
43 Q_PROPERTY(bool empty READ isEmpty NOTIFY emptyChanged)
37 Q_PROPERTY(QStringList availableApplications READ availableApplications NOTIFY availableApplicationsChanged)
44 Q_PROPERTY(QStringList availableApplications READ availableApplications NOTIFY availableApplicationsChanged)
@@ -40,13 +47,6 @@ class ApplicationManager : public ApplicationManagerInterface {
40 ApplicationManager(QObject *parent = nullptr);
47 ApplicationManager(QObject *parent = nullptr);
41 virtual ~ApplicationManager();
48 virtual ~ApplicationManager();
42
49
43− static ApplicationManager *singleton();
44−
45− enum MoreRoles {
46− RoleSession = RoleExemptFromLifecycle+1,
47− RoleFullscreen,
48− };
49−
50 // QAbstractItemModel methods.
50 // QAbstractItemModel methods.
51 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
51 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
52 QVariant data(const QModelIndex& index, int role) const override;
52 QVariant data(const QModelIndex& index, int role) const override;
@@ -57,8 +57,6 @@ class ApplicationManager : public ApplicationManagerInterface {
57
57
58 // Application control methods
58 // Application control methods
59 Q_INVOKABLE bool requestFocusApplication(const QString &appId) override;
59 Q_INVOKABLE bool requestFocusApplication(const QString &appId) override;
60− Q_INVOKABLE bool focusApplication(const QString &appId) override;
61− Q_INVOKABLE void unfocusCurrentApplication() override;
62 Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, const QStringList &arguments = QStringList()) override;
60 Q_INVOKABLE ApplicationInfo *startApplication(const QString &appId, const QStringList &arguments = QStringList()) override;
63 Q_INVOKABLE bool stopApplication(const QString &appId) override;
61 Q_INVOKABLE bool stopApplication(const QString &appId) override;
64
62
@@ -79,17 +77,17 @@ class ApplicationManager : public ApplicationManagerInterface {
79
77
80 private Q_SLOTS:
78 private Q_SLOTS:
81 void onWindowCreatedTimerTimeout();
79 void onWindowCreatedTimerTimeout();
80+ void updateFocusedApplication();
82
81
83 private:
82 private:
84− void add(ApplicationInfo *application);
83+ bool add(ApplicationInfo *application);
85 void remove(ApplicationInfo* application);
84 void remove(ApplicationInfo* application);
86 void buildListOfAvailableApplications();
85 void buildListOfAvailableApplications();
87 void onWindowCreated();
86 void onWindowCreated();
87+ ApplicationInfo *findApplication(MirSurface* surface);
88 QList<ApplicationInfo*> m_runningApplications;
88 QList<ApplicationInfo*> m_runningApplications;
89 QList<ApplicationInfo*> m_availableApplications;
89 QList<ApplicationInfo*> m_availableApplications;
90 QTimer m_windowCreatedTimer;
90 QTimer m_windowCreatedTimer;
91−
92− static ApplicationManager *the_application_manager;
93};
91};
94
92
95Q_DECLARE_METATYPE(ApplicationManager*)
93Q_DECLARE_METATYPE(ApplicationManager*)
tests/mocks/Unity/Application/ApplicationTestInterface.cppdeleted−117
@@ -1,117 +0,0 @@
1−/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "ApplicationInfo.h"
18−#include "ApplicationTestInterface.h"
19−#include "ApplicationManager.h"
20−#include "SessionManager.h"
21−#include "SurfaceManager.h"
22−#include "Session.h"
23−#include "MirSurface.h"
24−
25−#include <paths.h>
26−
27−#include <QtDBus/QtDBus>
28−
29−quint32 nextId = 0;
30−
31−ApplicationTestInterface::ApplicationTestInterface(QObject* parent)
32− : QDBusAbstractAdaptor(parent)
33−{
34− QDBusConnection connection = QDBusConnection::sessionBus();
35− connection.registerService("com.canonical.Unity8");
36− connection.registerObject("/com/canonical/Unity8/Mocks", parent);
37−}
38−
39−Session* ApplicationTestInterface::addChildSession(Session* existingSession, const QString& surfaceImage, bool addSurface)
40−{
41− if (!existingSession) return nullptr;
42−
43− QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%2@12.png")
44− .arg(qmlDirectory())
45− .arg(surfaceImage);
46−
47− Session* session = SessionManager::singleton()->createSession(
48− QString("%1-Child%2").arg(existingSession->name())
49− .arg(existingSession->childSessions()->count()),
50− screenshotUrl);
51− existingSession->addChildSession(session);
52− if (addSurface) session->createSurface();
53−
54− return session;
55−}
56−
57−void ApplicationTestInterface::removeSession(Session* existingSession)
58−{
59− qDebug() << "ApplicationTestInterface::removeSession - " << existingSession;
60−
61− if (existingSession)
62− existingSession->setLive(false);
63−}
64−
65−void ApplicationTestInterface::removeSurface(MirSurface* existingSurface)
66−{
67− qDebug() << "ApplicationTestInterface::removeSurface - " << existingSurface;
68−
69− if (existingSurface)
70− existingSurface->setLive(false);
71−}
72−
73−quint32 ApplicationTestInterface::addChildSession(const QString& appId, quint32 existingSessionId, const QString& surfaceImage, bool addSurface)
74−{
75− qDebug() << "ApplicationTestInterface::addChildSession to " << appId;
76−
77− ApplicationInfo* application = ApplicationManager::singleton()->findApplication(appId);
78− if (!application) {
79− qDebug() << "ApplicationTestInterface::addChildSession - No application found for " << appId;
80− return 0;
81− }
82−
83− Session* parentSession = nullptr;
84− if (m_childSessions.contains(existingSessionId)) {
85− parentSession = m_childSessions[existingSessionId];
86− } else if (application->session()) {
87− parentSession = application->session();
88− } else {
89− qDebug() << "ApplicationTestInterface::addChildSession - No session for " << appId << ":" << existingSessionId;
90− return 0;
91− }
92−
93− QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%2@12.png")
94− .arg(qmlDirectory())
95− .arg(surfaceImage);
96−
97− quint32 sessionId = ++nextId;
98− Session* session = SessionManager::singleton()->createSession(
99− QString("%1-Child%2").arg(parentSession->name())
100− .arg(parentSession->childSessions()->count()),
101− screenshotUrl);
102− parentSession->addChildSession(session);
103− if (addSurface) { session->createSurface(); }
104− m_childSessions[sessionId] = session;
105−
106− return sessionId;
107−}
108−
109−void ApplicationTestInterface::removeSession(quint32 sessionId)
110−{
111− if (!m_childSessions.contains(sessionId)) {
112− qDebug() << "ApplicationTestInterface::removeSession - No added session for " << sessionId;
113− return;
114− }
115− Session* session = m_childSessions.take(sessionId);
116− removeSession(session);
117−}
tests/mocks/Unity/Application/ApplicationTestInterface.hdeleted−45
@@ -1,45 +0,0 @@
1−/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef APPLICATIONTESTINTERFACE_H
18−#define APPLICATIONTESTINTERFACE_H
19−
20−#include <QtDBus/QtDBus>
21−
22−class ApplicationManager;
23−class Session;
24−class MirSurface;
25−
26−class ApplicationTestInterface : public QDBusAbstractAdaptor
27−{
28− Q_OBJECT
29− Q_CLASSINFO("D-Bus Interface", "com.canonical.Unity8.Mocks.Application")
30−public:
31− ApplicationTestInterface(QObject* parent = 0);
32−
33− Q_INVOKABLE Session* addChildSession(Session* existingSession, const QString& surfaceImage, bool addSurface);
34− Q_INVOKABLE void removeSession(Session* existingSession);
35− Q_INVOKABLE void removeSurface(MirSurface* existingSurface);
36−
37−public Q_SLOTS:
38− quint32 addChildSession(const QString& appId, quint32 existingSessionId, const QString& surfaceImage, bool addSurface);
39− void removeSession(quint32 sessionId);
40−
41−private:
42− QHash<quint32, Session*> m_childSessions;
43−};
44−
45−#endif // APPLICATIONTESTINTERFACE_H
tests/mocks/Unity/Application/CMakeLists.txt+3−4
@@ -2,14 +2,11 @@ set(FakeUnityApplicationQml_SOURCES
2 plugin.cpp
2 plugin.cpp
3 ApplicationInfo.cpp
3 ApplicationInfo.cpp
4 ApplicationManager.cpp
4 ApplicationManager.cpp
5− ApplicationTestInterface.cpp
6 MirSurface.cpp
5 MirSurface.cpp
7 MirSurfaceItem.cpp
6 MirSurfaceItem.cpp
7+ MirSurfaceListModel.cpp
8 ObjectListModel.h
8 ObjectListModel.h
9− Session.cpp
10− SessionManager.cpp
11 SurfaceManager.cpp
9 SurfaceManager.cpp
12− SessionModel.h
13 UbuntuKeyboardInfo.cpp
10 UbuntuKeyboardInfo.cpp
14 VirtualKeyboard.cpp
11 VirtualKeyboard.cpp
15 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
12 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
@@ -17,6 +14,8 @@ set(FakeUnityApplicationQml_SOURCES
17 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
14 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/Mir.h
18 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
15 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
19 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceItemInterface.h
16 ${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
20 resources/surfaces.qrc
19 resources/surfaces.qrc
21)
20)
22
21
tests/mocks/Unity/Application/MirSurface.cpp+175−10
@@ -19,13 +19,49 @@
19#include <QDebug>
19#include <QDebug>
20#include <QQmlEngine>
20#include <QQmlEngine>
21
21
22+#define MIRSURFACE_DEBUG 0
23+
24+#if MIRSURFACE_DEBUG
25+#define DEBUG_MSG(params) qDebug().nospace() << "MirSurface[" << (void*)this << "," << m_name << "]::" << __func__ << " " << params
26+
27+const char *stateToStr(Mir::State state)
28+{
29+ switch(state) {
30+ case Mir::UnknownState:
31+ return "unknown";
32+ case Mir::RestoredState:
33+ return "restored";
34+ case Mir::MinimizedState:
35+ return "minimized";
36+ case Mir::MaximizedState:
37+ return "maximized";
38+ case Mir::VertMaximizedState:
39+ return "vert-maximized";
40+ case Mir::FullscreenState:
41+ return "fullscreen";
42+ case Mir::HorizMaximizedState:
43+ return "horiz-maximized";
44+ case Mir::HiddenState:
45+ return "hidden";
46+ default:
47+ return "???";
48+ };
49+}
50+
51+#else
52+#define DEBUG_MSG(params) ((void)0)
53+#endif
54+
55+using namespace unity::shell::application;
56+
57+MirFocusController *MirFocusController::m_instance = nullptr;
58+
22MirSurface::MirSurface(const QString& name,
59MirSurface::MirSurface(const QString& name,
23 Mir::Type type,
60 Mir::Type type,
24 Mir::State state,
61 Mir::State state,
25 const QUrl& screenshot,
62 const QUrl& screenshot,
26− const QUrl &qmlFilePath,
63+ const QUrl &qmlFilePath)
27− QObject *parent)
28− : unity::shell::application::MirSurfaceInterface(parent)
64+ : unity::shell::application::MirSurfaceInterface(nullptr)
29 , m_name(name)
65 , m_name(name)
30 , m_type(type)
66 , m_type(type)
31 , m_state(state)
67 , m_state(state)
@@ -40,17 +76,28 @@ MirSurface::MirSurface(const QString& name,
40 , m_slowToResize(false)
76 , m_slowToResize(false)
41 , m_shellChrome(Mir::NormalChrome)
77 , m_shellChrome(Mir::NormalChrome)
42{
78{
43−// qDebug() << "MirSurface::MirSurface() " << name;
79+ DEBUG_MSG("");
80+
44 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
81 QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
45
82
46 m_delayedResizeTimer.setInterval(600);
83 m_delayedResizeTimer.setInterval(600);
47 m_delayedResizeTimer.setSingleShot(true);
84 m_delayedResizeTimer.setSingleShot(true);
48 connect(&m_delayedResizeTimer, &QTimer::timeout, this, &MirSurface::applyDelayedResize);
85 connect(&m_delayedResizeTimer, &QTimer::timeout, this, &MirSurface::applyDelayedResize);
86+
87+ m_zombieTimer.setInterval(100);
88+ m_zombieTimer.setSingleShot(true);
89+ connect(&m_zombieTimer, &QTimer::timeout, this, [this](){ this->setLive(false); });
49}
90}
50
91
51MirSurface::~MirSurface()
92MirSurface::~MirSurface()
52{
93{
53−// qDebug() << "MirSurface::~MirSurface() " << name();
94+ DEBUG_MSG("");
95+
96+ // controller instance might have been already destroyed by QQmlEngine destructor
97+ auto controller = MirFocusController::instance();
98+ if (controller && controller->focusedSurface() == this) {
99+ controller->clear();
100+ }
54}
101}
55
102
56QString MirSurface::name() const
103QString MirSurface::name() const
@@ -73,6 +120,7 @@ void MirSurface::setState(Mir::State state)
73 if (state == m_state)
120 if (state == m_state)
74 return;
121 return;
75
122
123+ DEBUG_MSG(stateToStr(state));
76 m_state = state;
124 m_state = state;
77 Q_EMIT stateChanged(state);
125 Q_EMIT stateChanged(state);
78}
126}
@@ -89,10 +137,10 @@ bool MirSurface::visible() const
89
137
90void MirSurface::setLive(bool live)
138void MirSurface::setLive(bool live)
91{
139{
92−// qDebug().nospace() << "MirSurface::setLive("<<live<<") " << name();
93 if (live == m_live)
140 if (live == m_live)
94 return;
141 return;
95
142
143+ DEBUG_MSG(live);
96 m_live = live;
144 m_live = live;
97 Q_EMIT liveChanged(live);
145 Q_EMIT liveChanged(live);
98
146
@@ -137,6 +185,7 @@ void MirSurface::setOrientationAngle(Mir::OrientationAngle angle)
137void MirSurface::setKeymap(const QString &value)
185void MirSurface::setKeymap(const QString &value)
138{
186{
139 if (value != m_keymap) {
187 if (value != m_keymap) {
188+ DEBUG_MSG(value);
140 m_keymap = value;
189 m_keymap = value;
141 Q_EMIT keymapChanged(m_keymap);
190 Q_EMIT keymapChanged(m_keymap);
142 }
191 }
@@ -157,6 +206,7 @@ void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
157 if (shellChrome == m_shellChrome)
206 if (shellChrome == m_shellChrome)
158 return;
207 return;
159
208
209+ DEBUG_MSG(shellChrome);
160 m_shellChrome = shellChrome;
210 m_shellChrome = shellChrome;
161 Q_EMIT shellChromeChanged(shellChrome);
211 Q_EMIT shellChromeChanged(shellChrome);
162}
212}
@@ -164,15 +214,13 @@ void MirSurface::setShellChrome(Mir::ShellChrome shellChrome)
164void MirSurface::registerView(qintptr viewId)
214void MirSurface::registerView(qintptr viewId)
165{
215{
166 m_views.insert(viewId, MirSurface::View{false});
216 m_views.insert(viewId, MirSurface::View{false});
167−// qDebug().nospace() << "MirSurface[" << name() << "]::registerView(" << viewId << ")"
168−// << " after=" << m_views.count();
217+ DEBUG_MSG(viewId << " after=" << m_views.count());
169}
218}
170
219
171void MirSurface::unregisterView(qintptr viewId)
220void MirSurface::unregisterView(qintptr viewId)
172{
221{
173−// qDebug().nospace() << "MirSurface[" << name() << "]::unregisterView(" << viewId << ")"
174−// << " after=" << m_views.count() << " live=" << m_live;
175 m_views.remove(viewId);
222 m_views.remove(viewId);
223+ DEBUG_MSG(viewId << " after=" << m_views.count() << " live=" << m_live);
176 if (!m_live && m_views.count() == 0) {
224 if (!m_live && m_views.count() == 0) {
177 deleteLater();
225 deleteLater();
178 }
226 }
@@ -288,6 +336,7 @@ bool MirSurface::isSlowToResize() const
288void MirSurface::setSlowToResize(bool value)
336void MirSurface::setSlowToResize(bool value)
289{
337{
290 if (m_slowToResize != value) {
338 if (m_slowToResize != value) {
339+ DEBUG_MSG(value);
291 m_slowToResize = value;
340 m_slowToResize = value;
292 Q_EMIT slowToResizeChanged();
341 Q_EMIT slowToResizeChanged();
293 if (!m_slowToResize && m_delayedResizeTimer.isActive()) {
342 if (!m_slowToResize && m_delayedResizeTimer.isActive()) {
@@ -344,3 +393,119 @@ void MirSurface::setHeightIncrement(int value)
344 Q_EMIT heightIncrementChanged(m_heightIncrement);
393 Q_EMIT heightIncrementChanged(m_heightIncrement);
345 }
394 }
346}
395}
396+
397+void MirSurface::raise()
398+{
399+ Q_EMIT raiseRequested();
400+}
401+
402+void MirSurface::close()
403+{
404+ DEBUG_MSG("");
405+ m_zombieTimer.start();
406+ Q_EMIT closeRequested();
407+}
408+
409+void MirSurface::createPromptSurface()
410+{
411+ QStringList screenshotIds = {"gallery", "map", "facebook", "camera", "browser", "music", "twitter"};
412+ int i = rand() % screenshotIds.count();
413+
414+ QUrl screenshotUrl = QString("qrc:///Unity/Application/screenshots/%1@12.png")
415+ .arg(screenshotIds[i]);
416+
417+ auto surface = new MirSurface(QString("prompt foo"),
418+ Mir::NormalType,
419+ Mir::RestoredState,
420+ screenshotUrl);
421+
422+ m_promptSurfaceList.appendSurface(surface);
423+}
424+
425+MirSurfaceListInterface* MirSurface::promptSurfaceList()
426+{
427+ return &m_promptSurfaceList;
428+}
429+
430+void MirSurface::requestFocus()
431+{
432+ DEBUG_MSG("");
433+ Q_EMIT focusRequested();
434+}
435+
436+void MirSurface::setFocused(bool value)
437+{
438+ DEBUG_MSG(value);
439+
440+ auto controller = MirFocusController::instance();
441+ // controller instance might have been already destroyed by QQmlEngine destructor
442+ if (!controller) {
443+ return;
444+ }
445+
446+ if (value) {
447+ controller->setFocusedSurface(this);
448+ } else if (controller->focusedSurface() == this) {
449+ controller->setFocusedSurface(nullptr);
450+ }
451+}
452+
453+bool MirSurface::focused() const
454+{
455+ auto controller = MirFocusController::instance();
456+
457+ // controller instance might have been already destroyed by QQmlEngine destructor
458+ return controller ? controller->focusedSurface() == this : false;
459+}
460+
461+#if MIRSURFACE_DEBUG
462+#undef DEBUG_MSG
463+#define DEBUG_MSG(params) qDebug().nospace() << "MirFocusController::" << __func__ << " " << params
464+#endif
465+
466+void MirFocusController::setFocusedSurface(MirSurfaceInterface *surface)
467+{
468+ if (m_focusedSurface == surface) {
469+ return;
470+ }
471+ DEBUG_MSG("MirSurface[" << (void*)surface << "," << (surface?surface->name():"") << "]");
472+
473+ m_previouslyFocusedSurface = m_focusedSurface;
474+ m_focusedSurface = surface;
475+
476+ if (m_previouslyFocusedSurface != m_focusedSurface) {
477+ Q_EMIT focusedSurfaceChanged();
478+ }
479+
480+ if (m_previouslyFocusedSurface) {
481+ Q_EMIT m_previouslyFocusedSurface->focusedChanged(false);
482+ }
483+
484+ if (m_focusedSurface) {
485+ Q_EMIT m_focusedSurface->focusedChanged(true);
486+ }
487+}
488+
489+MirFocusController* MirFocusController::instance()
490+{
491+ return m_instance;
492+}
493+
494+MirFocusController::MirFocusController()
495+{
496+ DEBUG_MSG("");
497+ Q_ASSERT(m_instance == nullptr);
498+ m_instance = this;
499+}
500+
501+MirFocusController::~MirFocusController()
502+{
503+ Q_ASSERT(m_instance == this);
504+ m_instance = nullptr;
505+}
506+
507+void MirFocusController::clear()
508+{
509+ m_focusedSurface = m_previouslyFocusedSurface = nullptr;
510+ Q_EMIT focusedSurfaceChanged();
511+}
tests/mocks/Unity/Application/MirSurface.h+51−7
@@ -23,8 +23,31 @@
23#include <QHash>
23#include <QHash>
24
24
25// unity-api
25// unity-api
26+#include <unity/shell/application/MirFocusControllerInterface.h>
26#include <unity/shell/application/MirSurfaceInterface.h>
27#include <unity/shell/application/MirSurfaceInterface.h>
27
28
29+#include "MirSurfaceListModel.h"
30+
31+class MirSurface;
32+
33+class MirFocusController : public unity::shell::application::MirFocusControllerInterface
34+{
35+ Q_OBJECT
36+public:
37+ MirFocusController();
38+ virtual ~MirFocusController();
39+ static MirFocusController* instance();
40+
41+ void setFocusedSurface(unity::shell::application::MirSurfaceInterface *surface) override;
42+ unity::shell::application::MirSurfaceInterface* focusedSurface() const override { return m_focusedSurface; }
43+ unity::shell::application::MirSurfaceInterface* previouslyFocusedSurface() { return m_previouslyFocusedSurface; }
44+ void clear();
45+private:
46+ static MirFocusController *m_instance;
47+ unity::shell::application::MirSurfaceInterface* m_previouslyFocusedSurface{nullptr};
48+ unity::shell::application::MirSurfaceInterface* m_focusedSurface{nullptr};
49+};
50+
28class MirSurface : public unity::shell::application::MirSurfaceInterface
51class MirSurface : public unity::shell::application::MirSurfaceInterface
29{
52{
30 Q_OBJECT
53 Q_OBJECT
@@ -41,8 +64,7 @@ public:
41 Mir::Type type,
64 Mir::Type type,
42 Mir::State state,
65 Mir::State state,
43 const QUrl& screenshot,
66 const QUrl& screenshot,
44− const QUrl &qmlFilePath = QUrl(),
67+ const QUrl &qmlFilePath = QUrl());
45− QObject *parent = nullptr);
46 virtual ~MirSurface();
68 virtual ~MirSurface();
47
69
48 ////
70 ////
@@ -79,17 +101,22 @@ public:
79
101
80 Mir::ShellChrome shellChrome() const override;
102 Mir::ShellChrome shellChrome() const override;
81
103
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+
82 ////
114 ////
83 // API for tests
115 // API for tests
84
116
85 Q_INVOKABLE void setLive(bool live);
117 Q_INVOKABLE void setLive(bool live);
86 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
118 Q_INVOKABLE void setShellChrome(Mir::ShellChrome shellChrome);
87
119
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−
93 int width() const;
120 int width() const;
94 int height() const;
121 int height() const;
95
122
@@ -103,6 +130,8 @@ public:
103 Q_INVOKABLE void setWidthIncrement(int);
130 Q_INVOKABLE void setWidthIncrement(int);
104 Q_INVOKABLE void setHeightIncrement(int);
131 Q_INVOKABLE void setHeightIncrement(int);
105
132
133+ Q_INVOKABLE void createPromptSurface();
134+
106 /////
135 /////
107 // internal mock stuff
136 // internal mock stuff
108
137
@@ -114,7 +143,16 @@ public:
114 bool activeFocus() const;
143 bool activeFocus() const;
115 void setActiveFocus(bool);
144 void setActiveFocus(bool);
116
145
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+
117Q_SIGNALS:
153Q_SIGNALS:
154+ ////
155+ // API for tests
118 void widthChanged();
156 void widthChanged();
119 void heightChanged();
157 void heightChanged();
120 void slowToResizeChanged();
158 void slowToResizeChanged();
@@ -123,6 +161,8 @@ Q_SIGNALS:
123 // internal mock stuff
161 // internal mock stuff
124 void screenshotUrlChanged(QUrl);
162 void screenshotUrlChanged(QUrl);
125 void activeFocusChanged(bool);
163 void activeFocusChanged(bool);
164+ void raiseRequested();
165+ void closeRequested();
126
166
127private Q_SLOTS:
167private Q_SLOTS:
128 void applyDelayedResize();
168 void applyDelayedResize();
@@ -135,6 +175,7 @@ private:
135 const Mir::Type m_type;
175 const Mir::Type m_type;
136 Mir::State m_state;
176 Mir::State m_state;
137 Mir::OrientationAngle m_orientationAngle;
177 Mir::OrientationAngle m_orientationAngle;
178+
138 QUrl m_screenshotUrl;
179 QUrl m_screenshotUrl;
139 QUrl m_qmlFilePath;
180 QUrl m_qmlFilePath;
140 bool m_live;
181 bool m_live;
@@ -158,11 +199,14 @@ private:
158 QSize m_pendingResize;
199 QSize m_pendingResize;
159
200
160 Mir::ShellChrome m_shellChrome;
201 Mir::ShellChrome m_shellChrome;
202+ MirSurfaceListModel m_promptSurfaceList;
161
203
162 struct View {
204 struct View {
163 bool visible;
205 bool visible;
164 };
206 };
165 QHash<qintptr, View> m_views;
207 QHash<qintptr, View> m_views;
208+
209+ QTimer m_zombieTimer;
166};
210};
167
211
168#endif // MOCK_MIR_SURFACE_H
212#endif // MOCK_MIR_SURFACE_H
tests/mocks/Unity/Application/MirSurfaceItem.cpp+13−7
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -29,6 +29,14 @@
29
29
30using namespace unity::shell::application;
30using namespace unity::shell::application;
31
31
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+
32MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
40MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
33 : MirSurfaceItemInterface(parent)
41 : MirSurfaceItemInterface(parent)
34 , m_qmlSurface(nullptr)
42 , m_qmlSurface(nullptr)
@@ -41,7 +49,7 @@ MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
41 , m_mousePressCount(0)
49 , m_mousePressCount(0)
42 , m_mouseReleaseCount(0)
50 , m_mouseReleaseCount(0)
43{
51{
44−// qDebug() << "MirSurfaceItem::MirSurfaceItem() " << (void*)(this) << name();
52+ DEBUG_MSG((void*)(this) << name());
45 setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton |
53 setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton |
46 Qt::ExtraButton1 | Qt::ExtraButton2 | Qt::ExtraButton3 | Qt::ExtraButton4 |
54 Qt::ExtraButton1 | Qt::ExtraButton2 | Qt::ExtraButton3 | Qt::ExtraButton4 |
47 Qt::ExtraButton5 | Qt::ExtraButton6 | Qt::ExtraButton7 | Qt::ExtraButton8 |
55 Qt::ExtraButton5 | Qt::ExtraButton6 | Qt::ExtraButton7 | Qt::ExtraButton8 |
@@ -53,7 +61,7 @@ MirSurfaceItem::MirSurfaceItem(QQuickItem *parent)
53
61
54MirSurfaceItem::~MirSurfaceItem()
62MirSurfaceItem::~MirSurfaceItem()
55{
63{
56−// qDebug() << "MirSurfaceItem::~MirSurfaceItem() " << (void*)(this) << name();
64+ DEBUG_MSG((void*)(this) << name());
57 setSurface(nullptr);
65 setSurface(nullptr);
58}
66}
59
67
@@ -150,7 +158,7 @@ void MirSurfaceItem::onComponentStatusChanged(QQmlComponent::Status status)
150
158
151void MirSurfaceItem::createQmlContentItem()
159void MirSurfaceItem::createQmlContentItem()
152{
160{
153−// qDebug() << "MirSurfaceItem::createQmlContentItem()";
161+ DEBUG_MSG("");
154
162
155 m_qmlItem = qobject_cast<QQuickItem*>(m_qmlContentComponent->create());
163 m_qmlItem = qobject_cast<QQuickItem*>(m_qmlContentComponent->create());
156 m_qmlItem->setParentItem(this);
164 m_qmlItem->setParentItem(this);
@@ -213,9 +221,7 @@ void MirSurfaceItem::mouseReleaseEvent(QMouseEvent * event)
213
221
214void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
222void MirSurfaceItem::setSurface(MirSurfaceInterface* surface)
215{
223{
216−// qDebug().nospace() << "MirSurfaceItem::setSurface() this=" << (void*)(this)
217−// << " name=" << name()
218−// << " surface=" << surface;
224+ DEBUG_MSG("this=" << (void*)(this) << " name=" << name() << " surface=" << surface);
219
225
220 if (m_qmlSurface == surface) {
226 if (m_qmlSurface == surface) {
221 return;
227 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 @@
20// Qt
20// Qt
21#include <QAbstractListModel>
21#include <QAbstractListModel>
22
22
23+#include <QDebug>
24+
23template<class TYPE>
25template<class TYPE>
24class ObjectListModel : public QAbstractListModel
26class ObjectListModel : public QAbstractListModel
25{
27{
tests/mocks/Unity/Application/Session.cppdeleted−209
@@ -1,209 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "Session.h"
18−#include "ApplicationInfo.h"
19−#include "SessionManager.h"
20−#include "SurfaceManager.h"
21−
22−#include <QPainter>
23−#include <QQmlEngine>
24−#include <QTimer>
25−
26−Session::Session(const QString &name,
27− const QUrl& screenshot,
28− QObject *parent)
29− : QObject(parent)
30− , m_name(name)
31− , m_live(true)
32− , m_screenshot(screenshot)
33− , m_application(nullptr)
34− , m_surface(nullptr)
35− , m_parentSession(nullptr)
36− , m_children(new SessionModel(this))
37− , m_fullscreen(false)
38−{
39−// qDebug() << "Session::Session() " << this->name();
40−
41− QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership);
42−}
43−
44−Session::~Session()
45−{
46−// qDebug() << "Session::~Session() " << name();
47−
48− QList<Session*> children(m_children->list());
49− for (Session* child : children) {
50− delete child;
51− }
52− if (m_parentSession) {
53− m_parentSession->removeChildSession(this);
54− }
55− if (m_application) {
56− m_application->setSession(nullptr);
57− }
58− delete m_children;
59−
60− if (m_surface) {
61− if (m_surface->viewCount() == 0) {
62− delete m_surface;
63− } else {
64− m_surface->setLive(false);
65− }
66− }
67−}
68−
69−void Session::release()
70−{
71−// qDebug() << "Session::release " << name();
72− deleteLater();
73−}
74−
75−void Session::updateFullscreenProperty()
76−{
77− if (m_surfaces.rowCount() > 0) {
78− // TODO: Figure out something better
79− setFullscreen(lastSurface()->state() == Mir::FullscreenState);
80− } else {
81− // Keep the current value of the fullscreen property until we get a new
82− // surface
83− }
84−}
85−
86−void Session::setFullscreen(bool fullscreen)
87−{
88− if (m_fullscreen != fullscreen) {
89− m_fullscreen = fullscreen;
90− Q_EMIT fullscreenChanged(m_fullscreen);
91− }
92−}
93−
94−void Session::setApplication(ApplicationInfo* application)
95−{
96− if (m_application == application)
97− return;
98−
99− m_application = application;
100− Q_EMIT applicationChanged(application);
101−}
102−
103−void Session::appendSurface(MirSurface* surface)
104−{
105− qDebug() << "Session::appendSurface - session=" << name() << "surface=" << surface;
106−
107− m_surfaces.insert(m_surfaces.rowCount(), surface);
108−
109− connect(surface, &MirSurfaceInterface::stateChanged, this, &Session::updateFullscreenProperty);
110− connect(surface, &QObject::destroyed,
111− this, [this, surface]() { this->removeSurface(surface); });
112−
113− Q_EMIT lastSurfaceChanged(surface);
114− Q_EMIT surfaceAdded(surface);
115−
116− updateFullscreenProperty();
117−}
118−
119−void Session::removeSurface(MirSurface* surface)
120−{
121− disconnect(surface, nullptr, this, nullptr);
122− if (m_surfaces.contains(surface)) {
123− m_surfaces.remove(surface);
124− }
125−
126− updateFullscreenProperty();
127−}
128−
129−void Session::setScreenshot(const QUrl& screenshot)
130−{
131− if (screenshot != m_screenshot) {
132− m_screenshot = screenshot;
133− if (m_surface) {
134− m_surface->setScreenshotUrl(m_screenshot);
135− }
136− }
137−}
138−
139−void Session::setLive(bool live)
140−{
141− if (m_live != live) {
142− m_live = live;
143− Q_EMIT liveChanged(m_live);
144− }
145−}
146−
147−void Session::setParentSession(Session* session)
148−{
149− if (m_parentSession == session || session == this)
150− return;
151−
152− m_parentSession = session;
153− Q_EMIT parentSessionChanged(session);
154−}
155−
156−void Session::createSurface()
157−{
158− if (m_surface) return;
159−
160− appendSurface(SurfaceManager::singleton()->createSurface(name(),
161− Mir::NormalType,
162− m_application && m_application->fullscreen() ? Mir::FullscreenState :
163− Mir::MaximizedState,
164− m_screenshot));
165−}
166−
167−void Session::addChildSession(Session* session)
168−{
169− insertChildSession(m_children->rowCount(), session);
170−}
171−
172−void Session::insertChildSession(uint index, Session* session)
173−{
174− qDebug() << "Session::insertChildSession - " << session->name() << " to " << name() << " @ " << index;
175−
176− session->setParentSession(this);
177− m_children->insert(index, session);
178− SessionManager::singleton()->registerSession(session);
179−}
180−
181−void Session::removeChildSession(Session* session)
182−{
183− qDebug() << "Session::removeChildSession - " << session->name() << " from " << name();
184−
185− if (m_children->contains(session)) {
186− m_children->remove(session);
187− session->setParentSession(nullptr);
188− Q_EMIT session->deregister();
189− }
190−}
191−
192−SessionModel* Session::childSessions() const
193−{
194− return m_children;
195−}
196−
197−ObjectListModel<MirSurface>* Session::surfaces() const
198−{
199− return const_cast<ObjectListModel<MirSurface>*>(&m_surfaces);
200−}
201−
202−MirSurface *Session::lastSurface() const
203−{
204− if (m_surfaces.rowCount() > 0) {
205− return m_surfaces.list().last();
206− } else {
207− return nullptr;
208− }
209−}
tests/mocks/Unity/Application/Session.hdeleted−104
@@ -1,104 +0,0 @@
1−/*
2− * Copyright (C) 2014,2015 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef SESSION_H
18−#define SESSION_H
19−
20−#include "ObjectListModel.h"
21−#include "SessionModel.h"
22−
23−#include <QQuickItem>
24−
25−class ApplicationInfo;
26−class MirSurface;
27−
28−class Session : public QObject
29−{
30− Q_OBJECT
31− Q_PROPERTY(QString name READ name CONSTANT)
32− Q_PROPERTY(MirSurface* lastSurface READ lastSurface NOTIFY lastSurfaceChanged)
33− Q_PROPERTY(ObjectListModel<MirSurface>* surfaces READ surfaces CONSTANT)
34− Q_PROPERTY(ApplicationInfo* application READ application NOTIFY applicationChanged)
35− Q_PROPERTY(Session* parentSession READ parentSession NOTIFY parentSessionChanged DESIGNABLE false)
36− Q_PROPERTY(SessionModel* childSessions READ childSessions DESIGNABLE false CONSTANT)
37− Q_PROPERTY(bool live READ live NOTIFY liveChanged)
38−
39−public:
40− explicit Session(const QString &name,
41− const QUrl& screenshot,
42− QObject *parent = 0);
43− ~Session();
44−
45− Q_INVOKABLE void release();
46−
47− //getters
48− QString name() const { return m_name; }
49− bool live() const { return m_live; }
50− bool fullscreen() const { return m_fullscreen; }
51− ApplicationInfo* application() const { return m_application; }
52− MirSurface *lastSurface() const;
53− ObjectListModel<MirSurface>* surfaces() const;
54− Session* parentSession() const { return m_parentSession; }
55−
56− void setApplication(ApplicationInfo* item);
57− void appendSurface(MirSurface* surface);
58− void removeSurface(MirSurface* surface);
59− void setScreenshot(const QUrl& m_screenshot);
60− void setLive(bool live);
61− void setFullscreen(bool fullscreen);
62−
63− Q_INVOKABLE void addChildSession(Session* session);
64− void insertChildSession(uint index, Session* session);
65− Q_INVOKABLE void removeChildSession(Session* session);
66−
67−Q_SIGNALS:
68− void applicationChanged(ApplicationInfo*);
69− void parentSessionChanged(Session*);
70− void liveChanged(bool live);
71− void surfaceAdded(MirSurface *surface);
72− void lastSurfaceChanged(MirSurface *surface);
73− void fullscreenChanged(bool fullscreen);
74−
75− // internal mock use
76− void deregister();
77−
78−public Q_SLOTS:
79− Q_INVOKABLE void createSurface();
80−
81−private Q_SLOTS:
82− void updateFullscreenProperty();
83−
84−private:
85− SessionModel* childSessions() const;
86− void setParentSession(Session* session);
87−
88− QString m_name;
89− bool m_live;
90− QUrl m_screenshot;
91− ApplicationInfo* m_application;
92− MirSurface* m_surface;
93− Session* m_parentSession;
94− SessionModel* m_children;
95− ObjectListModel<MirSurface> m_surfaces;
96− bool m_fullscreen;
97−
98− friend class ApplicationTestInterface;
99−};
100−
101−Q_DECLARE_METATYPE(Session*)
102−Q_DECLARE_METATYPE(ObjectListModel<MirSurface>*)
103−
104−#endif // SESSION_H
tests/mocks/Unity/Application/SessionManager.cppdeleted−55
@@ -1,55 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#include "SessionManager.h"
18−
19−#include "Session.h"
20−
21−SessionManager *SessionManager::the_session_manager = nullptr;
22−
23−SessionManager *SessionManager::singleton()
24−{
25− if (!the_session_manager) {
26− the_session_manager = new SessionManager();
27− }
28− return the_session_manager;
29−}
30−
31−SessionManager::SessionManager(QObject *parent) :
32− QObject(parent)
33−{
34−}
35−
36−
37−Session *SessionManager::createSession(const QString& name,
38− const QUrl& screenshot)
39−{
40− Session* session = new Session(name, screenshot);
41− Q_EMIT sessionStarting(session);
42− return session;
43−}
44−
45−void SessionManager::registerSession(Session *session)
46−{
47− connect(session, &Session::deregister, this, [this] {
48− Session* session = qobject_cast<Session*>(sender());
49−
50− disconnect(session, 0, this, 0);
51−
52− session->setLive(false);
53− Q_EMIT sessionStopping(session);
54− });
55−}
tests/mocks/Unity/Application/SessionModel.hdeleted−35
@@ -1,35 +0,0 @@
1−/*
2− * Copyright (C) 2014 Canonical, Ltd.
3− *
4− * This program is free software: you can redistribute it and/or modify it under
5− * the terms of the GNU Lesser General Public License version 3, as published by
6− * the Free Software Foundation.
7− *
8− * This program is distributed in the hope that it will be useful, but WITHOUT
9− * ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
10− * SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11− * Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−#ifndef SESSIONMODEL_H
18−#define SESSIONMODEL_H
19−
20−// Local
21−#include "ObjectListModel.h"
22−
23−class Session;
24−class SessionModel: public ObjectListModel<Session>
25−{
26− Q_OBJECT
27−public:
28− SessionModel(QObject *parent = 0): ObjectListModel(parent) {}
29−
30− Q_INVOKABLE int count() const { return rowCount(); }
31−};
32−
33−Q_DECLARE_METATYPE(SessionModel*)
34−
35−#endif // SESSIONMODEL_H
tests/mocks/Unity/Application/SurfaceManager.cpp+16−9
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -16,17 +16,15 @@
16
16
17#include "SurfaceManager.h"
17#include "SurfaceManager.h"
18
18
19+#include "ApplicationInfo.h"
19#include "VirtualKeyboard.h"
20#include "VirtualKeyboard.h"
20
21
21#include <paths.h>
22#include <paths.h>
22
23
23SurfaceManager *SurfaceManager::the_surface_manager = nullptr;
24SurfaceManager *SurfaceManager::the_surface_manager = nullptr;
24
25
25−SurfaceManager *SurfaceManager::singleton()
26+SurfaceManager *SurfaceManager::instance()
26{
27{
27− if (!the_surface_manager) {
28− the_surface_manager = new SurfaceManager();
29− }
30 return the_surface_manager;
28 return the_surface_manager;
31}
29}
32
30
@@ -34,7 +32,10 @@ SurfaceManager::SurfaceManager(QObject *parent) :
34 QObject(parent)
32 QObject(parent)
35 , m_virtualKeyboard(nullptr)
33 , m_virtualKeyboard(nullptr)
36{
34{
35+ Q_ASSERT(the_surface_manager == nullptr);
36+ the_surface_manager = this;
37+
37− m_virtualKeyboard = new VirtualKeyboard(this);
38+ m_virtualKeyboard = new VirtualKeyboard;
38 connect(m_virtualKeyboard, &QObject::destroyed, this, [this](QObject *obj) {
39 connect(m_virtualKeyboard, &QObject::destroyed, this, [this](QObject *obj) {
39 MirSurface* surface = qobject_cast<MirSurface*>(obj);
40 MirSurface* surface = qobject_cast<MirSurface*>(obj);
40 m_virtualKeyboard = nullptr;
41 m_virtualKeyboard = nullptr;
@@ -43,10 +44,16 @@ SurfaceManager::SurfaceManager(QObject *parent) :
43 });
44 });
44}
45}
45
46
47+SurfaceManager::~SurfaceManager()
48+{
49+ Q_ASSERT(the_surface_manager == this);
50+ the_surface_manager = nullptr;
51+}
52+
46MirSurface *SurfaceManager::createSurface(const QString& name,
53MirSurface *SurfaceManager::createSurface(const QString& name,
47− Mir::Type type,
54+ Mir::Type type,
48− Mir::State state,
55+ Mir::State state,
49− const QUrl& screenshot)
56+ const QUrl& screenshot)
50{
57{
51 MirSurface* surface = new MirSurface(name, type, state, screenshot);
58 MirSurface* surface = new MirSurface(name, type, state, screenshot);
52 connect(surface, &QObject::destroyed, this, [this](QObject *obj) {
59 connect(surface, &QObject::destroyed, this, [this](QObject *obj) {
tests/mocks/Unity/Application/SurfaceManager.h+5−2
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@
22#include "MirSurface.h"
22#include "MirSurface.h"
23#include "VirtualKeyboard.h"
23#include "VirtualKeyboard.h"
24
24
25+class ApplicationInfo;
26+
25class SurfaceManager : public QObject
27class SurfaceManager : public QObject
26{
28{
27 Q_OBJECT
29 Q_OBJECT
@@ -35,8 +37,9 @@ class SurfaceManager : public QObject
35
37
36public:
38public:
37 explicit SurfaceManager(QObject *parent = 0);
39 explicit SurfaceManager(QObject *parent = 0);
40+ virtual ~SurfaceManager();
38
41
39− static SurfaceManager *singleton();
42+ static SurfaceManager *instance();
40
43
41 Q_INVOKABLE MirSurface* createSurface(const QString& name,
44 Q_INVOKABLE MirSurface* createSurface(const QString& name,
42 Mir::Type type,
45 Mir::Type type,
tests/mocks/Unity/Application/UbuntuKeyboardInfo.cpp−2
@@ -16,8 +16,6 @@
16
16
17#include "UbuntuKeyboardInfo.h"
17#include "UbuntuKeyboardInfo.h"
18
18
19−UbuntuKeyboardInfo *UbuntuKeyboardInfo::m_instance = nullptr;
20−
21UbuntuKeyboardInfo::UbuntuKeyboardInfo(QObject *parent)
19UbuntuKeyboardInfo::UbuntuKeyboardInfo(QObject *parent)
22 : QObject(parent),
20 : QObject(parent),
23 m_x(0),
21 m_x(0),
tests/mocks/Unity/Application/UbuntuKeyboardInfo.h−9
@@ -40,13 +40,6 @@ public:
40 void setWidth(qreal value);
40 void setWidth(qreal value);
41 void setHeight(qreal value);
41 void setHeight(qreal value);
42
42
43− static UbuntuKeyboardInfo *singleton() {
44− if (!m_instance) {
45− m_instance = new UbuntuKeyboardInfo;
46− }
47− return m_instance;
48− }
49−
50Q_SIGNALS:
43Q_SIGNALS:
51 void xChanged(qreal x);
44 void xChanged(qreal x);
52 void yChanged(qreal y);
45 void yChanged(qreal y);
@@ -59,8 +52,6 @@ private:
59 qint32 m_y;
52 qint32 m_y;
60 qint32 m_width;
53 qint32 m_width;
61 qint32 m_height;
54 qint32 m_height;
62−
63− static UbuntuKeyboardInfo *m_instance;
64};
55};
65
56
66#endif // UBUNTU_KEYBOARD_INFO_H
57#endif // UBUNTU_KEYBOARD_INFO_H
tests/mocks/Unity/Application/VirtualKeyboard.cpp+2−3
@@ -22,13 +22,12 @@
22
22
23#include <QDebug>
23#include <QDebug>
24
24
25−VirtualKeyboard::VirtualKeyboard(QObject *parent)
25+VirtualKeyboard::VirtualKeyboard()
26 : MirSurface("input-method",
26 : MirSurface("input-method",
27 Mir::InputMethodType,
27 Mir::InputMethodType,
28 Mir::MinimizedState,
28 Mir::MinimizedState,
29 QUrl("qrc:///Unity/Application/vkb_portrait.png"),
29 QUrl("qrc:///Unity/Application/vkb_portrait.png"),
30− QUrl("qrc:///Unity/Application/VirtualKeyboard.qml"),
30+ QUrl("qrc:///Unity/Application/VirtualKeyboard.qml"))
31− parent)
32{
31{
33}
32}
34
33
tests/mocks/Unity/Application/VirtualKeyboard.h+1−1
@@ -25,7 +25,7 @@ class VirtualKeyboard : public MirSurface
25{
25{
26 Q_OBJECT
26 Q_OBJECT
27public:
27public:
28− explicit VirtualKeyboard(QObject *parent = nullptr);
28+ explicit VirtualKeyboard();
29 ~VirtualKeyboard();
29 ~VirtualKeyboard();
30};
30};
31
31
tests/mocks/Unity/Application/plugin.cpp+32−35
@@ -17,11 +17,8 @@
17#include "plugin.h"
17#include "plugin.h"
18#include "ApplicationInfo.h"
18#include "ApplicationInfo.h"
19#include "ApplicationManager.h"
19#include "ApplicationManager.h"
20−#include "Session.h"
21#include "MirSurfaceItem.h"
20#include "MirSurfaceItem.h"
22#include "SurfaceManager.h"
21#include "SurfaceManager.h"
23−#include "SessionManager.h"
24−#include "ApplicationTestInterface.h"
25#include "UbuntuKeyboardInfo.h"
22#include "UbuntuKeyboardInfo.h"
26
23
27// unity-api
24// unity-api
@@ -30,62 +27,65 @@
30#include <qqml.h>
27#include <qqml.h>
31#include <QQmlEngine>
28#include <QQmlEngine>
32
29
33−static QObject* applicationManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
30+namespace {
34− Q_UNUSED(engine);
35− Q_UNUSED(scriptEngine);
36− return ApplicationManager::singleton();
37−}
38
31
39−static QObject* surfaceManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
32+// Creates the singletons that are called throughout C++ code
40− Q_UNUSED(engine);
33+void createUnityApplicationSharedSingletons()
41− Q_UNUSED(scriptEngine);
34+{
35+ // they have to be created in a specific order
36+ if (!MirFocusController::instance()) {
37+ new MirFocusController;
38+ }
42− return SurfaceManager::singleton();
39+ if (!SurfaceManager::instance()) {
40+ new SurfaceManager;
41+ }
43}
42}
44
43
45−static QObject* sessionManagerSingleton(QQmlEngine* engine, QJSEngine* scriptEngine) {
44+QObject* applicationManagerSingleton(QQmlEngine*, QJSEngine*)
46− Q_UNUSED(engine);
45+{
47− Q_UNUSED(scriptEngine);
46+ createUnityApplicationSharedSingletons();
48− return SessionManager::singleton();
47+ return new ApplicationManager;
49}
48}
50
49
51−static QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*) {
50+QObject* surfaceManagerSingleton(QQmlEngine*, QJSEngine*)
52− return UbuntuKeyboardInfo::singleton();
51+{
52+ createUnityApplicationSharedSingletons();
53+ return SurfaceManager::instance();
53}
54}
54
55
56+QObject* ubuntuKeyboardInfoSingleton(QQmlEngine*, QJSEngine*)
57+{
58+ createUnityApplicationSharedSingletons();
59+ return new UbuntuKeyboardInfo;
60+}
55
61
56−ApplicationTestInterface* s_appTestInterface = nullptr;
57−
58−static QObject* applicationTestInterface(QQmlEngine* engine, QJSEngine* scriptEngine) {
62+QObject* mirFocusControllerSingleton(QQmlEngine*, QJSEngine*)
59− Q_UNUSED(engine);
63+{
60− Q_UNUSED(scriptEngine);
64+ createUnityApplicationSharedSingletons();
61− if (!s_appTestInterface) {
65+ return MirFocusController::instance();
62− s_appTestInterface = new ApplicationTestInterface(engine);
63− }
64− return s_appTestInterface;
65}
66}
66
67
68+} // anonymous namespace
69+
67void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
70void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
68{
71{
69− qRegisterMetaType<Session*>("Session*");
70 qRegisterMetaType<ApplicationInfo*>("ApplicationInfo*");
72 qRegisterMetaType<ApplicationInfo*>("ApplicationInfo*");
71− qRegisterMetaType<SessionModel*>("SessionModel*");
72 qRegisterMetaType<unity::shell::application::MirSurfaceInterface*>("unity::shell::application::MirSurfaceInterface*");
73 qRegisterMetaType<unity::shell::application::MirSurfaceInterface*>("unity::shell::application::MirSurfaceInterface*");
74+ qRegisterMetaType<unity::shell::application::MirSurfaceListInterface*>("unity::shell::application::MirSurfaceListInterface*");
73 qRegisterMetaType<Mir::Type>("Mir::Type");
75 qRegisterMetaType<Mir::Type>("Mir::Type");
74 qRegisterMetaType<Mir::State>("Mir::State");
76 qRegisterMetaType<Mir::State>("Mir::State");
75
77
76 qmlRegisterUncreatableType<unity::shell::application::ApplicationManagerInterface>(uri, 0, 1, "ApplicationManagerInterface", "Abstract interface. Cannot be created in QML");
78 qmlRegisterUncreatableType<unity::shell::application::ApplicationManagerInterface>(uri, 0, 1, "ApplicationManagerInterface", "Abstract interface. Cannot be created in QML");
77 qmlRegisterUncreatableType<unity::shell::application::ApplicationInfoInterface>(uri, 0, 1, "ApplicationInfoInterface", "Abstract interface. Cannot be created in QML");
79 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");
79 qmlRegisterUncreatableType<MirSurface>(uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
80 qmlRegisterUncreatableType<MirSurface>(uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
80 qmlRegisterUncreatableType<unity::shell::application::MirSurfaceInterface>(
81 qmlRegisterUncreatableType<unity::shell::application::MirSurfaceInterface>(
81 uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
82 uri, 0, 1, "MirSurface", "MirSurface can't be instantiated from QML");
83+ qmlRegisterSingletonType<MirFocusController>(uri, 0, 1, "MirFocusController", mirFocusControllerSingleton);
82 qmlRegisterType<MirSurfaceItem>(uri, 0, 1, "MirSurfaceItem");
84 qmlRegisterType<MirSurfaceItem>(uri, 0, 1, "MirSurfaceItem");
83 qmlRegisterType<ApplicationInfo>(uri, 0, 1, "ApplicationInfo");
85 qmlRegisterType<ApplicationInfo>(uri, 0, 1, "ApplicationInfo");
84
86
85 qmlRegisterSingletonType<ApplicationManager>(uri, 0, 1, "ApplicationManager", applicationManagerSingleton);
87 qmlRegisterSingletonType<ApplicationManager>(uri, 0, 1, "ApplicationManager", applicationManagerSingleton);
86 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
88 qmlRegisterSingletonType<SurfaceManager>(uri, 0, 1, "SurfaceManager", surfaceManagerSingleton);
87− qmlRegisterSingletonType<SessionManager>(uri, 0, 1, "SessionManager", sessionManagerSingleton);
88− qmlRegisterSingletonType<ApplicationTestInterface>(uri, 0, 1, "ApplicationTest", applicationTestInterface);
89 qmlRegisterSingletonType<UbuntuKeyboardInfo>(uri, 0, 1, "UbuntuKeyboardInfo", ubuntuKeyboardInfoSingleton);
89 qmlRegisterSingletonType<UbuntuKeyboardInfo>(uri, 0, 1, "UbuntuKeyboardInfo", ubuntuKeyboardInfoSingleton);
90
90
91 qmlRegisterUncreatableType<Mir>(uri, 0, 1, "Mir", "Mir provides enum values, it can't be instantiated");
91 qmlRegisterUncreatableType<Mir>(uri, 0, 1, "Mir", "Mir provides enum values, it can't be instantiated");
@@ -94,7 +94,4 @@ void FakeUnityApplicationQmlPlugin::registerTypes(const char *uri)
94void FakeUnityApplicationQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
94void FakeUnityApplicationQmlPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
95{
95{
96 QQmlExtensionPlugin::initializeEngine(engine, uri);
96 QQmlExtensionPlugin::initializeEngine(engine, uri);
97−
98− // make sure we initialise our test interface.
99− applicationTestInterface(engine, nullptr);
100}
97}
tests/mocks/Utils/CMakeLists.txt+4
@@ -22,6 +22,10 @@ set(QMLPLUGIN_SRC
22 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
22 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
23 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
23 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
24 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
24 ${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
25 constants.cpp
29 constants.cpp
26 plugin.cpp
30 plugin.cpp
27 windowstatestorage.cpp
31 windowstatestorage.cpp
tests/mocks/Utils/plugin.cpp+1−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
tests/plugins/Unity/Launcher/launchermodeltest.cpp+2−3
@@ -64,6 +64,7 @@ public:
64 void setExemptFromLifecycle(bool) override {}
64 void setExemptFromLifecycle(bool) override {}
65 QSize initialSurfaceSize() const override { return QSize(); }
65 QSize initialSurfaceSize() const override { return QSize(); }
66 void setInitialSurfaceSize(const QSize &) override {}
66 void setInitialSurfaceSize(const QSize &) override {}
67+ MirSurfaceListInterface* surfaceList() override { return nullptr; }
67
68
68 // Methods used for mocking (not in the interface)
69 // Methods used for mocking (not in the interface)
69 void setFocused(bool focused) { m_focused = focused; Q_EMIT focusedChanged(focused); }
70 void setFocused(bool focused) { m_focused = focused; Q_EMIT focusedChanged(focused); }
@@ -106,7 +107,7 @@ public:
106 }
107 }
107 return false;
108 return false;
108 }
109 }
109− bool focusApplication(const QString &appId) override {
110+ bool focusApplication(const QString &appId) {
110 Q_FOREACH(MockApp* app, m_list) {
111 Q_FOREACH(MockApp* app, m_list) {
111 app->setFocused(app->appId() == appId);
112 app->setFocused(app->appId() == appId);
112 }
113 }
@@ -114,8 +115,6 @@ public:
114 return true;
115 return true;
115 }
116 }
116
117
117− void unfocusCurrentApplication() override { }
118−
119 void addApplication(MockApp *app) {
118 void addApplication(MockApp *app) {
120 beginInsertRows(QModelIndex(), count(), count());
119 beginInsertRows(QModelIndex(), count(), count());
121 m_list.append(app);
120 m_list.append(app);
tests/qmltests/CMakeLists.txt−1
@@ -88,7 +88,6 @@ add_unity8_qmltest(Stages DesktopStage)
88add_unity8_qmltest(Stages PhoneStage)
88add_unity8_qmltest(Stages PhoneStage)
89add_unity8_qmltest(Stages SpreadDelegate)
89add_unity8_qmltest(Stages SpreadDelegate)
90add_unity8_qmltest(Stages SurfaceContainer)
90add_unity8_qmltest(Stages SurfaceContainer)
91−add_unity8_qmltest(Stages SessionContainer)
92add_unity8_qmltest(Stages TabletStage)
91add_unity8_qmltest(Stages TabletStage)
93add_unity8_qmltest(Stages WindowResizeArea)
92add_unity8_qmltest(Stages WindowResizeArea)
94add_unity8_qmltest(Stages Splash)
93add_unity8_qmltest(Stages Splash)
tests/qmltests/Dash/tst_DashShell.qml+6−2
@@ -91,8 +91,12 @@ Item {
91 var stopY = startY;
91 var stopY = startY;
92 touchFlick(shell, startX, startY, stopX, stopY);
92 touchFlick(shell, startX, startY, stopX, stopY);
93
93
94− // Now do a long launcher movement
94+ // bring in the launcher
95+ var startX = units.gu(1);
96+ var startY = shell.height / 2;
97+ var stopX = shell.width / 3;
98+ var stopY = startY;
95− touchFlick(shell, stopX, startY, startX, stopY);
99+ touchFlick(shell, startX, startY, stopX, stopY);
96
100
97 var launcher = findChild(shell, "launcher");
101 var launcher = findChild(shell, "launcher");
98 var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
102 var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
tests/qmltests/Greeter/tst_Greeter.qml+2−2
@@ -361,7 +361,7 @@ Item {
361 }
361 }
362
362
363 function test_notifyAboutToFocusApp() {
363 function test_notifyAboutToFocusApp() {
364− greeter.notifyAboutToFocusApp("fake-app");
364+ greeter.notifyUserRequestedApp("fake-app");
365 compare(viewTryToUnlockSpy.count, 1);
365 compare(viewTryToUnlockSpy.count, 1);
366 compare(viewTryToUnlockSpy.signalArguments[0][0], false);
366 compare(viewTryToUnlockSpy.signalArguments[0][0], false);
367 }
367 }
@@ -500,7 +500,7 @@ Item {
500
500
501 // Test opening a locked app
501 // Test opening a locked app
502 greeter.lockedApp = "test-app";
502 greeter.lockedApp = "test-app";
503− greeter.notifyAppFocused("test-app");
503+ greeter.notifyAppFocusRequested("test-app");
504 verify(greeter.hasLockedApp);
504 verify(greeter.hasLockedApp);
505 verify(!greeter.shown);
505 verify(!greeter.shown);
506
506
tests/qmltests/Panel/tst_ActiveCallHint.qml+3−2
@@ -96,11 +96,12 @@ Item {
96 function test_activeHint(data) {
96 function test_activeHint(data) {
97 if (data.dialer) {
97 if (data.dialer) {
98 var application = ApplicationManager.startApplication("dialer-app");
98 var application = ApplicationManager.startApplication("dialer-app");
99− ApplicationManager.focusApplication("dialer-app");
99+ tryCompare(application.surfaceList, "count", 1);
100+ MirFocusController.focusedSurface = application.surfaceList.get(0);
100 tryCompare(ApplicationManager, "focusedApplicationId", "dialer-app");
101 tryCompare(ApplicationManager, "focusedApplicationId", "dialer-app");
101 tryCompare(application, "state", ApplicationInfoInterface.Running);
102 tryCompare(application, "state", ApplicationInfoInterface.Running);
102
103
103− if (!data.focused) { ApplicationManager.unfocusCurrentApplication(); }
104+ if (!data.focused) { MirFocusController.focusedSurface = null; }
104 }
105 }
105 callManager.foregroundCall = data.call;
106 callManager.foregroundCall = data.call;
106 callManager.callIndicatorVisible = data.visible;
107 callManager.callIndicatorVisible = data.visible;
tests/qmltests/Panel/tst_Panel.qml+2−2
@@ -29,7 +29,7 @@ IndicatorTest {
29 id: root
29 id: root
30 width: units.gu(100)
30 width: units.gu(100)
31 height: units.gu(71)
31 height: units.gu(71)
32− color: "white"
32+ color: "black"
33
33
34 Component.onCompleted: theme.name = "Ubuntu.Components.Themes.SuruDark"
34 Component.onCompleted: theme.name = "Ubuntu.Components.Themes.SuruDark"
35
35
@@ -464,7 +464,7 @@ IndicatorTest {
464
464
465 callManager.foregroundCall = phoneCall;
465 callManager.foregroundCall = phoneCall;
466
466
467− ApplicationManager.focusApplication("unity8-dash");
467+ MirFocusController.focusedSurface = ApplicationManager.findApplication("unity8-dash").surfaceList.get(0);
468 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
468 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
469
469
470 mouseClick(panel.indicators,
470 mouseClick(panel.indicators,
tests/qmltests/Stages/ApplicationCheckBox.qml+104−58
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -19,11 +19,17 @@ import QtQuick.Layouts 1.1
19import Ubuntu.Components 1.3
19import Ubuntu.Components 1.3
20import Unity.Application 0.1
20import Unity.Application 0.1
21
21
22−RowLayout {
22+Column {
23 id: root
23 id: root
24 property string appId
24 property string appId
25 property bool checked: false
25 property bool checked: false
26
26
27+ function createSurface() {
28+ if (d.application) {
29+ d.application.createSurface();
30+ }
31+ }
32+
27 enabled: appId !== "unity8-dash"
33 enabled: appId !== "unity8-dash"
28
34
29 onCheckedChanged: {
35 onCheckedChanged: {
@@ -38,77 +44,117 @@ RowLayout {
38 d.bindGuard = false;
44 d.bindGuard = false;
39 }
45 }
40
46
47+ RowLayout {
48+
41− QtObject {
49+ QtObject {
42− id: d
50+ id: d
43− property bool bindGuard: false
51+ property bool bindGuard: false
44− property var application: null
52+ property var application: null
45− Component.onCompleted: {
53+ Component.onCompleted: {
46− application = ApplicationManager.findApplication(root.appId);
54+ application = ApplicationManager.findApplication(root.appId);
55+ }
47 }
56 }
48− }
49
57
50− Connections {
58+ Connections {
51− target: ApplicationManager
59+ target: ApplicationManager
52− onCountChanged: {
60+ onCountChanged: {
53− d.application = ApplicationManager.findApplication(root.appId);
61+ d.application = ApplicationManager.findApplication(root.appId);
62+ }
54 }
63 }
55− }
56
64
57− Layout.fillWidth: true
65+ Layout.fillWidth: true
58− CheckBox {
66+ CheckBox {
59− id: checkbox
67+ id: checkbox
60− checked: false
68+ checked: false
61− activeFocusOnPress: false
69+ activeFocusOnPress: false
62
70
63− onTriggered: {
71+ onTriggered: {
64− if (d.bindGuard) { return; }
72+ if (d.bindGuard) { return; }
65− d.bindGuard = true;
73+ d.bindGuard = true;
66
74
67− if (checked) {
75+ if (checked) {
68− ApplicationManager.startApplication(root.appId);
76+ ApplicationManager.startApplication(root.appId);
69− } else {
77+ } else {
70− ApplicationManager.stopApplication(root.appId);
78+ ApplicationManager.stopApplication(root.appId);
79+ }
80+ d.bindGuard = false;
71 }
81 }
72− d.bindGuard = false;
73− }
74− onCheckedChanged: {
82+ onCheckedChanged: {
75− if (d.bindGuard) { return; }
83+ if (d.bindGuard) { return; }
76− d.bindGuard = true;
84+ d.bindGuard = true;
77
85
78− root.checked = checked;
86+ root.checked = checked;
79
87
80− d.bindGuard = false;
88+ d.bindGuard = false;
89+ }
90+ Binding {
91+ target: checkbox
92+ property: "checked"
93+ value: d.application != null
94+ }
81 }
95 }
82− Binding {
96+ Label {
83− target: checkbox
97+ id: appIdLabel
84− property: "checked"
98+ text: root.appId
85− value: d.application != null
99+ color: "white"
100+ anchors.verticalCenter: parent.verticalCenter
86 }
101 }
87− }
88− Label {
89− id: appIdLabel
90− text: root.appId
91− color: "white"
92− anchors.verticalCenter: parent.verticalCenter
93− }
94− Rectangle {
102+ Rectangle {
95− color: {
103+ color: {
96− if (d.application) {
104+ if (d.application) {
97− if (d.application.state === ApplicationInfoInterface.Starting) {
105+ if (d.application.state === ApplicationInfoInterface.Starting) {
98− return "yellow";
106+ return "yellow";
99− } else if (d.application.state === ApplicationInfoInterface.Running) {
107+ } else if (d.application.state === ApplicationInfoInterface.Running) {
100− return "green";
108+ return "green";
101− } else if (d.application.state === ApplicationInfoInterface.Suspended) {
109+ } else if (d.application.state === ApplicationInfoInterface.Suspended) {
102− return "blue";
110+ return "blue";
111+ } else {
112+ return "darkred";
113+ }
103 } else {
114 } else {
104 return "darkred";
115 return "darkred";
105 }
116 }
106− } else {
117+ }
107− return "darkred";
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+ }
108 }
157 }
109 }
158 }
110− width: height
111− height: appIdLabel.height * 0.7
112− anchors.verticalCenter: parent.verticalCenter
113 }
159 }
114}
160}
tests/qmltests/Stages/RecursingChildSessionControl.qml+10−61
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2014,2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -22,16 +22,7 @@ import Unity.Application 0.1
22ColumnLayout {
22ColumnLayout {
23 id: root
23 id: root
24
24
25− property var session
25+ property var surface
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− }
35
26
36 Column {
27 Column {
37 id: column
28 id: column
@@ -41,7 +32,7 @@ ColumnLayout {
41
32
42 Repeater {
33 Repeater {
43 id: repeater
34 id: repeater
44− model: root.childSessions
35+ model: root.surface ? root.surface.promptSurfaceList : null
45 delegate: Rectangle {
36 delegate: Rectangle {
46 border {
37 border {
47 color: "black"
38 color: "black"
@@ -71,8 +62,7 @@ ColumnLayout {
71 }
62 }
72
63
73 onLoaded: {
64 onLoaded: {
74− item.session = modelData;
65+ item.surface = model.surface;
75− item.removable = true;
76 }
66 }
77 }
67 }
78 }
68 }
@@ -83,62 +73,21 @@ ColumnLayout {
83 Layout.fillWidth: true
73 Layout.fillWidth: true
84 spacing: units.gu(1)
74 spacing: units.gu(1)
85
75
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−
116 RowLayout {
76 RowLayout {
117 id: buttonLayout
77 id: buttonLayout
118
78
119 Button {
79 Button {
120− enabled: root.session
80+ enabled: root.surface
121 activeFocusOnPress: false
81 activeFocusOnPress: false
122− text: removable ? "Remove" : "Release"
82+ text: "Remove"
123− onClicked: {
83+ onClicked: { root.surface.close(); }
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− }
132 }
84 }
133
85
134 Button {
86 Button {
135− enabled: root.session !== null
87+ enabled: root.surface !== null
136 activeFocusOnPress: false
88 activeFocusOnPress: false
137− text: "Add Child"
89+ text: "Add Prompt Surface"
138− onClicked: {
90+ onClicked: { root.surface.createPromptSurface(); }
139− var screenshot = Math.round(Math.random()*100 % (screenshotIds.length-1));
140− var session = ApplicationTest.addChildSession(root.session, root.screenshotIds[screenshot], true);
141− }
142 }
91 }
143 }
92 }
144 }
93 }
tests/qmltests/Stages/tst_ApplicationWindow.qml+73−59
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -34,16 +34,9 @@ Rectangle {
34 root.fakeApplication = ApplicationManager.add("gallery-app");
34 root.fakeApplication = ApplicationManager.add("gallery-app");
35 root.fakeApplication.manualSurfaceCreation = true;
35 root.fakeApplication.manualSurfaceCreation = true;
36 root.fakeApplication.setState(ApplicationInfoInterface.Starting);
36 root.fakeApplication.setState(ApplicationInfoInterface.Starting);
37+ applicationWindowLoader.item.application = root.fakeApplication;
37 }
38 }
38 property QtObject fakeApplication: null
39 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− }
47
40
48 Loader {
41 Loader {
49 id: applicationWindowLoader
42 id: applicationWindowLoader
@@ -58,7 +51,6 @@ Rectangle {
58 sourceComponent: Component {
51 sourceComponent: Component {
59 ApplicationWindow {
52 ApplicationWindow {
60 anchors.fill: parent
53 anchors.fill: parent
61− application: fakeApplication
62 surfaceOrientationAngle: 0
54 surfaceOrientationAngle: 0
63 interactive: true
55 interactive: true
64 focus: true
56 focus: true
@@ -86,18 +78,27 @@ Rectangle {
86 Layout.fillWidth: true
78 Layout.fillWidth: true
87
79
88 CheckBox {
80 CheckBox {
81+ id: surfaceCheckbox;
89− id: sessionCheckbox; checked: false
82+ checked: false;
90 activeFocusOnPress: false
83 activeFocusOnPress: false
91 onCheckedChanged: {
84 onCheckedChanged: {
92− if (checked && !fakeApplication.session) {
85+ if (applicationWindowLoader.status !== Loader.Ready)
93− fakeApplication.createSession();
86+ return;
94− } else if (!checked && fakeApplication.session) {
87+
95− fakeApplication.destroySession();
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+ }
96 }
97 }
97 }
98 }
98 }
99 }
99 Label {
100 Label {
100− text: "Session"
101+ text: "Surface"
101 anchors.verticalCenter: parent.verticalCenter
102 anchors.verticalCenter: parent.verticalCenter
102 }
103 }
103 }
104 }
@@ -117,7 +118,7 @@ Rectangle {
117 id: sessionControl
118 id: sessionControl
118 anchors { left: parent.left; right: parent.right; }
119 anchors { left: parent.left; right: parent.right; }
119
120
120− session: fakeSession
121+ surface: applicationWindowLoader.item ? applicationWindowLoader.item.surface : null
121 }
122 }
122 }
123 }
123
124
@@ -143,7 +144,15 @@ Rectangle {
143 }
144 }
144 onSelectedApplicationStateChanged: {
145 onSelectedApplicationStateChanged: {
145 // state is a read-only property, thus we have to call the setter function
146 // state is a read-only property, thus we have to call the setter function
147+ if (fakeApplication && fakeApplication.state != selectedApplicationState) {
146− fakeApplication.setState(selectedApplicationState);
148+ fakeApplication.setState(selectedApplicationState);
149+ }
150+ }
151+ Connections {
152+ target: fakeApplication
153+ onStateChanged: {
154+ testCase.setApplicationState(state);
155+ }
147 }
156 }
148 }
157 }
149
158
@@ -197,6 +206,8 @@ Rectangle {
197 }
206 }
198 }
207 }
199
208
209+ property var applicationWindow: applicationWindowLoader.item
210+
200 // holds some of the internal ApplicationWindow objects we probe during the tests
211 // holds some of the internal ApplicationWindow objects we probe during the tests
201 property var stateGroup: null
212 property var stateGroup: null
202
213
@@ -213,11 +224,6 @@ Rectangle {
213 findInterestingApplicationWindowChildren();
224 findInterestingApplicationWindowChildren();
214 }
225 }
215
226
216− function initSession() {
217− sessionCheckbox.checked = true;
218− sessionControl.surfaceCheckbox.checked = true;
219− }
220−
221 function cleanup() {
227 function cleanup() {
222 forgetApplicationWindowChildren();
228 forgetApplicationWindowChildren();
223
229
@@ -238,10 +244,17 @@ Rectangle {
238 // deleted, so you end up with two instances in memory.
244 // deleted, so you end up with two instances in memory.
239 tryCompare(applicationWindowLoader, "itemDestroyed", true);
245 tryCompare(applicationWindowLoader, "itemDestroyed", true);
240
246
241− appStateSelector.selectedIndex = 0;
247+ setApplicationState(appStarting);
242− sessionCheckbox.checked = false;
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);
243
254
244 applicationWindowLoader.active = true;
255 applicationWindowLoader.active = true;
256+
257+ applicationWindowLoader.item.application = root.fakeApplication;
245 }
258 }
246
259
247 function test_showSplashUntilAppFullyInit_data() {
260 function test_showSplashUntilAppFullyInit_data() {
@@ -256,7 +269,7 @@ Rectangle {
256 verify(stateGroup.state === "splashScreen");
269 verify(stateGroup.state === "splashScreen");
257
270
258 if (data.swapInitOrder) {
271 if (data.swapInitOrder) {
259− initSession();
272+ surfaceCheckbox.checked = true;
260 } else {
273 } else {
261 setApplicationState(appRunning);
274 setApplicationState(appRunning);
262 }
275 }
@@ -266,14 +279,14 @@ Rectangle {
266 if (data.swapInitOrder) {
279 if (data.swapInitOrder) {
267 setApplicationState(appRunning);
280 setApplicationState(appRunning);
268 } else {
281 } else {
269− initSession();
282+ surfaceCheckbox.checked = true;
270 }
283 }
271
284
272 tryCompare(stateGroup, "state", "surface");
285 tryCompare(stateGroup, "state", "surface");
273 }
286 }
274
287
275 function test_suspendedAppShowsSurface() {
288 function test_suspendedAppShowsSurface() {
276− initSession();
289+ surfaceCheckbox.checked = true;
277 setApplicationState(appRunning);
290 setApplicationState(appRunning);
278
291
279 tryCompare(stateGroup, "state", "surface");
292 tryCompare(stateGroup, "state", "surface");
@@ -287,7 +300,7 @@ Rectangle {
287 }
300 }
288
301
289 function test_killedAppShowsScreenshot() {
302 function test_killedAppShowsScreenshot() {
290− initSession();
303+ surfaceCheckbox.checked = true;
291 setApplicationState(appRunning);
304 setApplicationState(appRunning);
292 tryCompare(stateGroup, "state", "surface");
305 tryCompare(stateGroup, "state", "surface");
293
306
@@ -297,16 +310,17 @@ Rectangle {
297 verify(fakeApplication.surface !== null);
310 verify(fakeApplication.surface !== null);
298
311
299 // kill it!
312 // kill it!
313+ surfaceCheckbox.checked = false;
300 setApplicationState(appStopped);
314 setApplicationState(appStopped);
301
315
302 tryCompare(stateGroup, "state", "screenshot");
316 tryCompare(stateGroup, "state", "screenshot");
303− tryCompare(fakeApplication, "session", null);
317+ tryCompare(fakeApplication.surfaceList, "count", 0);
304 }
318 }
305
319
306 function test_restartApp() {
320 function test_restartApp() {
307− var screenshotImage = findChild(applicationWindowLoader.item, "screenshotImage");
321+ var screenshotImage = findChild(applicationWindow, "screenshotImage");
308
322
309− initSession();
323+ surfaceCheckbox.checked = true;
310 setApplicationState(appRunning);
324 setApplicationState(appRunning);
311 tryCompare(stateGroup, "state", "surface");
325 tryCompare(stateGroup, "state", "surface");
312 waitUntilTransitionsEnd(stateGroup);
326 waitUntilTransitionsEnd(stateGroup);
@@ -314,109 +328,109 @@ Rectangle {
314 setApplicationState(appSuspended);
328 setApplicationState(appSuspended);
315
329
316 // kill it
330 // kill it
331+ surfaceCheckbox.checked = false;
317 setApplicationState(appStopped);
332 setApplicationState(appStopped);
318
333
319 tryCompare(stateGroup, "state", "screenshot");
334 tryCompare(stateGroup, "state", "screenshot");
320 waitUntilTransitionsEnd(stateGroup);
335 waitUntilTransitionsEnd(stateGroup);
321− tryCompare(fakeApplication, "session", null);
336+ tryCompare(applicationWindow, "surface", null);
322
337
323 // and restart it
338 // and restart it
324 setApplicationState(appStarting);
339 setApplicationState(appStarting);
325
340
326 waitUntilTransitionsEnd(stateGroup);
341 waitUntilTransitionsEnd(stateGroup);
327 verify(stateGroup.state === "screenshot");
342 verify(stateGroup.state === "screenshot");
328− verify(fakeSession === null);
343+ verify(applicationWindow.surface === null);
329
344
330 setApplicationState(appRunning);
345 setApplicationState(appRunning);
331
346
332 waitUntilTransitionsEnd(stateGroup);
347 waitUntilTransitionsEnd(stateGroup);
333 verify(stateGroup.state === "screenshot");
348 verify(stateGroup.state === "screenshot");
334
349
335− initSession();
350+ surfaceCheckbox.checked = true;
336
351
337 tryCompare(stateGroup, "state", "surface");
352 tryCompare(stateGroup, "state", "surface");
338 tryCompare(screenshotImage, "status", Image.Null);
353 tryCompare(screenshotImage, "status", Image.Null);
339 }
354 }
340
355
341 function test_appCrashed() {
356 function test_appCrashed() {
342− initSession();
357+ surfaceCheckbox.checked = true;
343 setApplicationState(appRunning);
358 setApplicationState(appRunning);
344 tryCompare(stateGroup, "state", "surface");
359 tryCompare(stateGroup, "state", "surface");
345 waitUntilTransitionsEnd(stateGroup);
360 waitUntilTransitionsEnd(stateGroup);
346
361
347 // oh, it crashed...
362 // oh, it crashed...
363+ surfaceCheckbox.checked = false;
348 setApplicationState(appStopped);
364 setApplicationState(appStopped);
349
365
350 tryCompare(stateGroup, "state", "screenshot");
366 tryCompare(stateGroup, "state", "screenshot");
351− tryCompare(fakeApplication, "session", null);
367+ tryCompare(applicationWindow, "surface", null);
352 }
368 }
353
369
354 function test_keepSurfaceWhileInvisible() {
370 function test_keepSurfaceWhileInvisible() {
355− initSession();
371+ surfaceCheckbox.checked = true;
356 setApplicationState(appRunning);
372 setApplicationState(appRunning);
357 tryCompare(stateGroup, "state", "surface");
373 tryCompare(stateGroup, "state", "surface");
358 waitUntilTransitionsEnd(stateGroup);
374 waitUntilTransitionsEnd(stateGroup);
359− verify(fakeSession.lastSurface !== null);
375+ verify(applicationWindow.surface !== null);
360
376
361− applicationWindowLoader.item.visible = false;
377+ applicationWindow.visible = false;
362
378
363 waitUntilTransitionsEnd(stateGroup);
379 waitUntilTransitionsEnd(stateGroup);
364 verify(stateGroup.state === "surface");
380 verify(stateGroup.state === "surface");
365− verify(fakeSession.lastSurface !== null);
381+ verify(applicationWindow.surface !== null);
366
382
367− applicationWindowLoader.item.visible = true;
383+ applicationWindow.visible = true;
368
384
369 waitUntilTransitionsEnd(stateGroup);
385 waitUntilTransitionsEnd(stateGroup);
370 verify(stateGroup.state === "surface");
386 verify(stateGroup.state === "surface");
371− verify(fakeSession.lastSurface !== null);
387+ verify(applicationWindow.surface !== null);
372 }
388 }
373
389
374 function test_touchesReachSurfaceWhenItsShown() {
390 function test_touchesReachSurfaceWhenItsShown() {
375 setApplicationState(appRunning);
391 setApplicationState(appRunning);
376− initSession();
392+ surfaceCheckbox.checked = true;
377
393
378 tryCompare(stateGroup, "state", "surface");
394 tryCompare(stateGroup, "state", "surface");
379
395
380 waitUntilTransitionsEnd(stateGroup);
396 waitUntilTransitionsEnd(stateGroup);
381
397
382− var surfaceItem = findChild(applicationWindowLoader.item, "surfaceItem");
398+ var surfaceItem = findChild(applicationWindow, "surfaceItem");
383 verify(surfaceItem);
399 verify(surfaceItem);
384− verify(surfaceItem.surface === fakeSession.lastSurface);
400+ verify(surfaceItem.surface === applicationWindow.surface);
385
401
386 surfaceItem.touchPressCount = 0;
402 surfaceItem.touchPressCount = 0;
387 surfaceItem.touchReleaseCount = 0;
403 surfaceItem.touchReleaseCount = 0;
388
404
389− tap(applicationWindowLoader.item,
405+ tap(applicationWindow);
390− applicationWindowLoader.item.width / 2, applicationWindowLoader.item.height / 2);
391
406
392 verify(surfaceItem.touchPressCount === 1);
407 verify(surfaceItem.touchPressCount === 1);
393 verify(surfaceItem.touchReleaseCount === 1);
408 verify(surfaceItem.touchReleaseCount === 1);
394 }
409 }
395
410
396 function test_showNothingOnSuddenSurfaceLoss() {
411 function test_showNothingOnSuddenSurfaceLoss() {
397− initSession();
412+ surfaceCheckbox.checked = true;
398 setApplicationState(appRunning);
413 setApplicationState(appRunning);
399 tryCompare(stateGroup, "state", "surface");
414 tryCompare(stateGroup, "state", "surface");
400 waitUntilTransitionsEnd(stateGroup);
415 waitUntilTransitionsEnd(stateGroup);
401
416
402− sessionControl.surfaceCheckbox.checked = false;
417+ applicationWindow.surface = null;
403
418
404 tryCompare(stateGroup, "state", "void");
419 tryCompare(stateGroup, "state", "void");
405 }
420 }
406
421
407 function test_surfaceActiveFocusFollowsAppWindowInterative() {
422 function test_surfaceActiveFocusFollowsAppWindowInterative() {
408− fakeApplication.createSession();
409− applicationWindowLoader.item.interactive = false;
423+ applicationWindow.interactive = false;
410− applicationWindowLoader.item.interactive = true;
424+ applicationWindow.interactive = true;
411− fakeSession.createSurface();
425+ surfaceCheckbox.checked = true;
412
426
413− compare(fakeSession.lastSurface.activeFocus, true);
427+ compare(applicationWindow.surface.activeFocus, true);
414
428
415− applicationWindowLoader.item.interactive = false;
429+ applicationWindow.interactive = false;
416− compare(fakeSession.lastSurface.activeFocus, false);
430+ compare(applicationWindow.surface.activeFocus, false);
417
431
418− applicationWindowLoader.item.interactive = true;
432+ applicationWindow.interactive = true;
419− compare(fakeSession.lastSurface.activeFocus, true);
433+ compare(applicationWindow.surface.activeFocus, true);
420 }
434 }
421 }
435 }
422}
436}
tests/qmltests/Stages/tst_DesktopStage.qml+243−161
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@ import Ubuntu.Components 1.3
20import Ubuntu.Components.ListItems 1.3
20import Ubuntu.Components.ListItems 1.3
21import Unity.Application 0.1
21import Unity.Application 0.1
22import Unity.Test 0.1
22import Unity.Test 0.1
23+import WindowManager 0.1
23import Utils 0.1
24import Utils 0.1
24
25
25import ".." // For EdgeBarrierControls
26import ".." // For EdgeBarrierControls
@@ -53,6 +54,11 @@ Item {
53 }
54 }
54 }
55 }
55
56
57+ TopLevelSurfaceList {
58+ id: topSurfaceList
59+ applicationsModel: ApplicationManager
60+ }
61+
56 Loader {
62 Loader {
57 id: desktopStageLoader
63 id: desktopStageLoader
58 x: ((root.width - controls.width) - width) / 2
64 x: ((root.width - controls.width) - width) / 2
@@ -68,6 +74,7 @@ Item {
68 color: "white"
74 color: "white"
69 anchors.fill: parent
75 anchors.fill: parent
70 background: "../../../qml/graphics/tablet_background.jpg"
76 background: "../../../qml/graphics/tablet_background.jpg"
77+ focus: true
71
78
72 Component.onCompleted: {
79 Component.onCompleted: {
73 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
80 edgeBarrierControls.target = testCase.findChild(this, "edgeBarrierController");
@@ -76,6 +83,8 @@ Item {
76 desktopStageLoader.itemDestroyed = true;
83 desktopStageLoader.itemDestroyed = true;
77 }
84 }
78 orientations: Orientations {}
85 orientations: Orientations {}
86+ applicationManager: ApplicationManager
87+ topLevelSurfaceList: topSurfaceList
79 }
88 }
80 }
89 }
81 }
90 }
@@ -103,7 +112,7 @@ Item {
103 activeFocusOnPress: false
112 activeFocusOnPress: false
104 onClicked: {
113 onClicked: {
105 if (ApplicationManager.focusedApplicationId) {
114 if (ApplicationManager.focusedApplicationId) {
106− var surface = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).session.lastSurface;
115+ var surface = ApplicationManager.findApplication(ApplicationManager.focusedApplicationId).surfaceList.get(0);
107 surface.slowToResize = true;
116 surface.slowToResize = true;
108 }
117 }
109 }
118 }
@@ -137,6 +146,19 @@ Item {
137
146
138 property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
147 property Item desktopStage: desktopStageLoader.status === Loader.Ready ? desktopStageLoader.item : null
139
148
149+ function init() {
150+ // wait until unity8-dash is up and running.
151+ // it's started automatically by ApplicationManager mock implementation
152+ tryCompare(ApplicationManager, "count", 1);
153+ var dashApp = ApplicationManager.findApplication("unity8-dash");
154+ verify(dashApp);
155+ tryCompare(dashApp, "state", ApplicationInfoInterface.Running);
156+
157+ tryCompare(topSurfaceList, "count", 1);
158+ tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) != null;}, true);
159+ compare(MirFocusController.focusedSurface, topSurfaceList.surfaceAt(0));
160+ }
161+
140 function cleanup() {
162 function cleanup() {
141 desktopStageLoader.itemDestroyed = false;
163 desktopStageLoader.itemDestroyed = false;
142 desktopStageLoader.active = false;
164 desktopStageLoader.active = false;
@@ -149,43 +171,71 @@ Item {
149 // Shell instance gets destroyed.
171 // Shell instance gets destroyed.
150 tryCompare(desktopStageLoader, "itemDestroyed", true);
172 tryCompare(desktopStageLoader, "itemDestroyed", true);
151
173
152− killAllRunningApps();
174+ killApps();
175+
176+ root.resetGeometry();
153
177
154 desktopStageLoader.active = true;
178 desktopStageLoader.active = true;
155 tryCompare(desktopStageLoader, "status", Loader.Ready);
179 tryCompare(desktopStageLoader, "status", Loader.Ready);
156− root.resetGeometry();
157− }
158
180
159− function killAllRunningApps() {
160− while (ApplicationManager.count > 0) {
161− ApplicationManager.stopApplication(ApplicationManager.get(0).appId);
162− }
163− compare(ApplicationManager.count, 0)
164 }
181 }
165
182
166− function waitUntilAppSurfaceShowsUp(appId) {
183+ function waitUntilAppSurfaceShowsUp(surfaceId) {
167− var appWindow = findChild(desktopStage, "appWindow_" + appId);
184+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
185+ verify(appDelegate);
186+ var appWindow = findChild(appDelegate, "appWindow");
168 verify(appWindow);
187 verify(appWindow);
169 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
188 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
170 verify(appWindowStates);
189 verify(appWindowStates);
171 tryCompare(appWindowStates, "state", "surface");
190 tryCompare(appWindowStates, "state", "surface");
172 }
191 }
173
192
193+ /*
194+ Returns the appDelegate of the first surface created by the app with the specified appId
195+ */
174 function startApplication(appId) {
196 function startApplication(appId) {
197+ try {
175− var app = ApplicationManager.findApplication(appId);
198+ var app = ApplicationManager.findApplication(appId);
176− if (!app) {
199+ if (app) {
200+ for (var i = 0; i < topSurfaceList.count; i++) {
201+ if (topSurfaceList.applicationAt(i).appId === appId) {
202+ var appRepeater = findChild(desktopStage, "appRepeater");
203+ verify(appRepeater);
204+ return appRepeater.itemAt(i);
205+ }
206+ }
207+ }
208+
209+ var surfaceId = topSurfaceList.nextId;
177 app = ApplicationManager.startApplication(appId);
210 app = ApplicationManager.startApplication(appId);
211+ verify(app);
212+ waitUntilAppSurfaceShowsUp(surfaceId);
213+ compare(app.surfaceList.count, 1);
214+
215+ return findChild(desktopStage, "appDelegate_" + surfaceId);
216+ } catch(err) {
217+ throw new Error("startApplication("+appId+") called from line " + util.callerLine(1) + " failed!");
218+ }
219+ }
220+
221+ function maximizeAppDelegate(appDelegate) {
222+ var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
223+ if (!maximizeButton) {
224+ fail("Could not find maximizeWindowButton of appDelegate=" + appDelegate);
225+ }
226+ mouseClick(maximizeButton);
227+
228+ try {
229+ tryCompare(appDelegate, "visuallyMaximized", true);
230+ } catch(err) {
231+ fail("appDelegate.visuallyMaximized !== true");
178 }
232 }
179− verify(app);
180− waitUntilAppSurfaceShowsUp(appId);
181− verify(app.session.lastSurface);
182− return app;
183 }
233 }
184
234
185 function test_appFocusSwitch_data() {
235 function test_appFocusSwitch_data() {
186 return [
236 return [
187− {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 0, focusTo: 1 },
237+ {tag: "dash to dialer", apps: [ "unity8-dash", "dialer-app", "gmail-webapp" ], focusfrom: 0, focusTo: 1 },
188− {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "camera-app" ], focusfrom: 1, focusTo: 0 },
238+ {tag: "dialer to dash", apps: [ "unity8-dash", "dialer-app", "gmail-webapp" ], focusfrom: 1, focusTo: 0 },
189 ]
239 ]
190 }
240 }
191
241
@@ -193,10 +243,10 @@ Item {
193 data.apps.forEach(startApplication);
243 data.apps.forEach(startApplication);
194
244
195 ApplicationManager.requestFocusApplication(data.apps[data.focusfrom]);
245 ApplicationManager.requestFocusApplication(data.apps[data.focusfrom]);
196− tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.lastSurface, "activeFocus", true);
246+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).surfaceList.get(0), "activeFocus", true);
197
247
198 ApplicationManager.requestFocusApplication(data.apps[data.focusTo]);
248 ApplicationManager.requestFocusApplication(data.apps[data.focusTo]);
199− tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.lastSurface, "activeFocus", true);
249+ tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).surfaceList.get(0), "activeFocus", true);
200 }
250 }
201
251
202 function test_tappingOnWindowChangesFocusedApp_data() {
252 function test_tappingOnWindowChangesFocusedApp_data() {
@@ -207,21 +257,24 @@ Item {
207 }
257 }
208
258
209 function test_tappingOnWindowChangesFocusedApp(data) {
259 function test_tappingOnWindowChangesFocusedApp(data) {
260+ var appDelegates = [];
261+ for (var i = 0; i < data.apps.length; i++) {
210− data.apps.forEach(startApplication);
262+ appDelegates[i] = startApplication(data.apps[i]);
263+ }
211− var fromAppId = data.apps[data.focusfrom];
264+ var fromDelegate = appDelegates[data.focusfrom];
212− var toAppId = data.apps[data.focusTo]
265+ var toDelegate = appDelegates[data.focusTo];
213
266
214− var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
267+ var fromAppWindow = findChild(fromDelegate, "appWindow");
215 verify(fromAppWindow);
268 verify(fromAppWindow);
216 tap(fromAppWindow);
269 tap(fromAppWindow);
217− compare(fromAppWindow.application.session.lastSurface.activeFocus, true);
270+ compare(fromDelegate.surface.activeFocus, true);
218− compare(ApplicationManager.focusedApplicationId, fromAppId);
271+ compare(MirFocusController.focusedSurface, fromDelegate.surface);
219
272
220− var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
273+ var toAppWindow = findChild(toDelegate, "appWindow");
221 verify(toAppWindow);
274 verify(toAppWindow);
222 tap(toAppWindow);
275 tap(toAppWindow);
223− compare(toAppWindow.application.session.lastSurface.activeFocus, true);
276+ compare(toDelegate.surface.activeFocus, true);
224− compare(ApplicationManager.focusedApplicationId, toAppId);
277+ compare(MirFocusController.focusedSurface, toDelegate.surface);
225 }
278 }
226
279
227 function test_clickingOnWindowChangesFocusedApp_data() {
280 function test_clickingOnWindowChangesFocusedApp_data() {
@@ -229,21 +282,24 @@ Item {
229 }
282 }
230
283
231 function test_clickingOnWindowChangesFocusedApp(data) {
284 function test_clickingOnWindowChangesFocusedApp(data) {
285+ var appDelegates = [];
286+ for (var i = 0; i < data.apps.length; i++) {
232− data.apps.forEach(startApplication);
287+ appDelegates[i] = startApplication(data.apps[i]);
288+ }
233− var fromAppId = data.apps[data.focusfrom];
289+ var fromDelegate = appDelegates[data.focusfrom];
234− var toAppId = data.apps[data.focusTo]
290+ var toDelegate = appDelegates[data.focusTo];
235
291
236− var fromAppWindow = findChild(desktopStage, "appWindow_" + fromAppId);
292+ var fromAppWindow = findChild(fromDelegate, "appWindow");
237 verify(fromAppWindow);
293 verify(fromAppWindow);
238 mouseClick(fromAppWindow);
294 mouseClick(fromAppWindow);
239− compare(fromAppWindow.application.session.lastSurface.activeFocus, true);
295+ compare(fromDelegate.surface.activeFocus, true);
240− compare(ApplicationManager.focusedApplicationId, fromAppId);
296+ compare(MirFocusController.focusedSurface, fromDelegate.surface);
241
297
242− var toAppWindow = findChild(desktopStage, "appWindow_" + toAppId);
298+ var toAppWindow = findChild(toDelegate, "appWindow");
243 verify(toAppWindow);
299 verify(toAppWindow);
244 mouseClick(toAppWindow);
300 mouseClick(toAppWindow);
245− compare(toAppWindow.application.session.lastSurface.activeFocus, true);
301+ compare(toDelegate.surface.activeFocus, true);
246− compare(ApplicationManager.focusedApplicationId, toAppId);
302+ compare(MirFocusController.focusedSurface, toDelegate.surface);
247 }
303 }
248
304
249 function test_tappingOnDecorationFocusesApplication_data() {
305 function test_tappingOnDecorationFocusesApplication_data() {
@@ -253,24 +309,49 @@ Item {
253 ]
309 ]
254 }
310 }
255
311
312+ function findDecoratedWindow(surfaceId) {
313+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
314+ if (!appDelegate) {
315+ console.warn("Could not find appDelegate for surfaceId="+surfaceId);
316+ return null;
317+ }
318+ return findChild(appDelegate, "decoratedWindow");
319+ }
320+
321+ function findWindowDecoration(surfaceId) {
322+ var appDelegate = findChild(desktopStage, "appDelegate_" + surfaceId);
323+ if (!appDelegate) {
324+ console.warn("Could not find appDelegate for surfaceId="+surfaceId);
325+ return null;
326+ }
327+ return findChild(appDelegate, "appWindowDecoration");
328+ }
329+
256 function test_tappingOnDecorationFocusesApplication(data) {
330 function test_tappingOnDecorationFocusesApplication(data) {
331+ var appDelegates = [];
332+ for (var i = 0; i < data.apps.length; i++) {
257− data.apps.forEach(startApplication);
333+ appDelegates[i] = startApplication(data.apps[i]);
334+ }
335+
336+ var fromDelegate = appDelegates[data.focusfrom];
337+ var toDelegate = appDelegates[data.focusTo];
258
338
259− var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
339+ var fromAppDecoration = findChild(fromDelegate, "appWindowDecoration");
260 verify(fromAppDecoration);
340 verify(fromAppDecoration);
261 tap(fromAppDecoration);
341 tap(fromAppDecoration);
262
342
263− var fromApp = ApplicationManager.findApplication(data.apps[data.focusfrom]);
264− verify(fromApp);
265− tryCompare(fromApp.session.lastSurface, "activeFocus", true);
343+ tryCompare(fromDelegate.surface, "activeFocus", true);
266
344
267− var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
345+ var toAppDecoration = findChild(toDelegate, "appWindowDecoration");
268 verify(toAppDecoration);
346 verify(toAppDecoration);
347+
348+ // FIXME: Wait a bit before the second tap or the window decoration will say it got double clicked.
349+ // No idea why.
350+ wait(700);
351+
269 tap(toAppDecoration);
352 tap(toAppDecoration);
270
353
271− var toApp = ApplicationManager.findApplication(data.apps[data.focusTo]);
272− verify(toApp);
273− tryCompare(toApp.session.lastSurface, "activeFocus", true);
354+ tryCompare(toDelegate.surface, "activeFocus", true);
274 }
355 }
275
356
276 function test_clickingOnDecorationFocusesApplication_data() {
357 function test_clickingOnDecorationFocusesApplication_data() {
@@ -278,159 +359,170 @@ Item {
278 }
359 }
279
360
280 function test_clickingOnDecorationFocusesApplication(data) {
361 function test_clickingOnDecorationFocusesApplication(data) {
362+ var appDelegates = [];
363+ for (var i = 0; i < data.apps.length; i++) {
281− data.apps.forEach(startApplication);
364+ appDelegates[i] = startApplication(data.apps[i]);
365+ }
366+ var fromDelegate = appDelegates[data.focusfrom];
367+ var toDelegate = appDelegates[data.focusTo];
282
368
283− var fromAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusfrom]);
369+ var fromAppDecoration = findChild(fromDelegate, "appWindowDecoration");
284 verify(fromAppDecoration);
370 verify(fromAppDecoration);
285 mouseClick(fromAppDecoration);
371 mouseClick(fromAppDecoration);
286− tryCompare(ApplicationManager.findApplication(data.apps[data.focusfrom]).session.lastSurface, "activeFocus", true);
372+ tryCompare(fromDelegate.surface, "activeFocus", true);
287
373
288− var toAppDecoration = findChild(desktopStage, "appWindowDecoration_" + data.apps[data.focusTo]);
374+ var toAppDecoration = findChild(toDelegate, "appWindowDecoration");
289 verify(toAppDecoration);
375 verify(toAppDecoration);
290 mouseClick(toAppDecoration);
376 mouseClick(toAppDecoration);
291− tryCompare(ApplicationManager.findApplication(data.apps[data.focusTo]).session.lastSurface, "activeFocus", true);
377+ tryCompare(toDelegate.surface, "activeFocus", true);
292 }
378 }
293
379
294 function test_windowMaximize() {
380 function test_windowMaximize() {
295− var apps = ["unity8-dash", "dialer-app", "camera-app"];
381+ var dialerDelegate = startApplication("dialer-app");
296− apps.forEach(startApplication);
382+ startApplication("camera-app");
297− var appName = "dialer-app";
383+
298− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
384+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
299− verify(appDelegate);
385+ dialerDelegate.surface.requestFocus();
300− ApplicationManager.focusApplication(appName);
386+ tryCompare(dialerDelegate, "focus", true);
387+
301 keyClick(Qt.Key_Up, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Up shortcut to maximize
388 keyClick(Qt.Key_Up, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Up shortcut to maximize
302− tryCompare(appDelegate, "maximized", true);
389+ tryCompare(dialerDelegate, "maximized", true);
303− tryCompare(appDelegate, "minimized", false);
390+ tryCompare(dialerDelegate, "minimized", false);
304 }
391 }
305
392
306 function test_windowMaximizeLeft() {
393 function test_windowMaximizeLeft() {
307− var apps = ["unity8-dash", "dialer-app", "camera-app"];
394+ var dialerDelegate = startApplication("dialer-app");
308− apps.forEach(startApplication);
395+ startApplication("camera-app");
309− var appName = "dialer-app";
396+
310− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
397+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
311− verify(appDelegate);
398+ dialerDelegate.surface.requestFocus();
312− ApplicationManager.focusApplication(appName);
399+ tryCompare(dialerDelegate, "focus", true);
400+
313 keyClick(Qt.Key_Left, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Left shortcut to maximizeLeft
401 keyClick(Qt.Key_Left, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Left shortcut to maximizeLeft
314− tryCompare(appDelegate, "maximized", false);
402+ tryCompare(dialerDelegate, "maximized", false);
315− tryCompare(appDelegate, "minimized", false);
403+ tryCompare(dialerDelegate, "minimized", false);
316− tryCompare(appDelegate, "maximizedLeft", true);
404+ tryCompare(dialerDelegate, "maximizedLeft", true);
317− tryCompare(appDelegate, "maximizedRight", false);
405+ tryCompare(dialerDelegate, "maximizedRight", false);
318 }
406 }
319
407
320 function test_windowMaximizeRight() {
408 function test_windowMaximizeRight() {
321− var apps = ["unity8-dash", "dialer-app", "camera-app"];
409+ var dialerDelegate = startApplication("dialer-app");
322− apps.forEach(startApplication);
410+ startApplication("camera-app");
323− var appName = "dialer-app";
411+
324− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
412+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
325− verify(appDelegate);
413+ dialerDelegate.surface.requestFocus();
326− ApplicationManager.focusApplication(appName);
414+ tryCompare(dialerDelegate, "focus", true);
415+
327 keyClick(Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Right shortcut to maximizeRight
416 keyClick(Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Right shortcut to maximizeRight
328− tryCompare(appDelegate, "maximized", false);
417+ tryCompare(dialerDelegate, "maximized", false);
329− tryCompare(appDelegate, "minimized", false);
418+ tryCompare(dialerDelegate, "minimized", false);
330− tryCompare(appDelegate, "maximizedLeft", false);
419+ tryCompare(dialerDelegate, "maximizedLeft", false);
331− tryCompare(appDelegate, "maximizedRight", true);
420+ tryCompare(dialerDelegate, "maximizedRight", true);
332 }
421 }
333
422
334 function test_windowMinimize() {
423 function test_windowMinimize() {
335− var apps = ["unity8-dash", "dialer-app", "camera-app"];
424+ var dialerDelegate = startApplication("dialer-app");
336− apps.forEach(startApplication);
425+ startApplication("camera-app");
337− var appName = "dialer-app";
426+
338− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
427+ tryCompareFunction(function(){ return dialerDelegate.surface !== null; }, true);
339− verify(appDelegate);
428+ dialerDelegate.surface.requestFocus();
340− ApplicationManager.focusApplication(appName);
429+ tryCompare(dialerDelegate, "focus", true);
430+
341 keyClick(Qt.Key_Down, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Down shortcut to minimize
431 keyClick(Qt.Key_Down, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+Down shortcut to minimize
342− tryCompare(appDelegate, "maximized", false);
432+ tryCompare(dialerDelegate, "maximized", false);
343− tryCompare(appDelegate, "minimized", true);
433+ tryCompare(dialerDelegate, "minimized", true);
344 verify(ApplicationManager.focusedApplicationId != ""); // verify we don't lose focus when minimizing an app
434 verify(ApplicationManager.focusedApplicationId != ""); // verify we don't lose focus when minimizing an app
345 }
435 }
346
436
347 function test_windowMinimizeAll() {
437 function test_windowMinimizeAll() {
348 var apps = ["unity8-dash", "dialer-app", "camera-app"];
438 var apps = ["unity8-dash", "dialer-app", "camera-app"];
349 apps.forEach(startApplication);
439 apps.forEach(startApplication);
350− verify(ApplicationManager.count == 3);
440+ verify(topSurfaceList.count == 3);
351 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
441 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
352 tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
442 tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
353 }
443 }
354
444
355 function test_windowClose() {
445 function test_windowClose() {
356− var apps = ["unity8-dash", "dialer-app", "camera-app"];
446+ var dialerSurfaceId = topSurfaceList.nextId;
357− apps.forEach(startApplication);
447+ var dialerDelegate = startApplication("dialer-app");
448+ verify(topSurfaceList.indexForId(dialerSurfaceId) !== -1);
449+ startApplication("gmail-webapp");
358− verify(ApplicationManager.count == 3);
450+ verify(topSurfaceList.count == 3);
359− var appName = "dialer-app";
451+
360− var appDelegate = findChild(desktopStage, "appDelegate_" + appName);
452+ mouseClick(dialerDelegate);
361− verify(appDelegate);
453+
362− ApplicationManager.focusApplication(appName);
454+ var desktopStagePriv = findInvisibleChild(desktopStage, "DesktopStagePrivate");
455+ tryCompare(desktopStagePriv, "focusedAppDelegate", dialerDelegate);
456+
363 keyClick(Qt.Key_F4, Qt.AltModifier); // Alt+F4 shortcut to close
457 keyClick(Qt.Key_F4, Qt.AltModifier); // Alt+F4 shortcut to close
458+
364− verify(ApplicationManager.count == 2); // verify the app is gone
459+ // verify the surface is gone
365− verify(ApplicationManager.findApplication(appName) === null); // and it's not in running apps
460+ tryCompare(topSurfaceList, "count", 2);
461+ verify(topSurfaceList.indexForId(dialerSurfaceId) === -1);
366 }
462 }
367
463
368 function test_smashCursorKeys() {
464 function test_smashCursorKeys() {
369− var apps = ["unity8-dash", "dialer-app", "camera-app"];
465+ var apps = ["dialer-app", "gmail-webapp"];
370 apps.forEach(startApplication);
466 apps.forEach(startApplication);
371− verify(ApplicationManager.count == 3);
467+ verify(topSurfaceList.count == 3);
372 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
468 keyClick(Qt.Key_D, Qt.MetaModifier|Qt.ControlModifier); // Ctrl+Super+D shortcut to minimize all
373− tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify no app is focused
469+ tryCompare(MirFocusController, "focusedSurface", null); // verify no surface is focused
374
470
375 // now try pressing all 4 arrow keys + ctrl + meta
471 // now try pressing all 4 arrow keys + ctrl + meta
376 keyClick(Qt.Key_Up | Qt.Key_Down | Qt.Key_Left | Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // smash it!!!
472 keyClick(Qt.Key_Up | Qt.Key_Down | Qt.Key_Left | Qt.Key_Right, Qt.MetaModifier|Qt.ControlModifier); // smash it!!!
377− tryCompare(ApplicationManager, "focusedApplicationId", ""); // verify still no app is focused
473+ tryCompare(MirFocusController, "focusedSurface", null); // verify still no surface is focused
378 }
474 }
379
475
380 function test_minimizeApplicationHidesSurface() {
476 function test_minimizeApplicationHidesSurface() {
381− var dashApp = startApplication("unity8-dash");
477+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
478+ var dashSurface = topSurfaceList.surfaceAt(0);
479+ var dashSurfaceId = topSurfaceList.idAt(0);
382
480
383− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
481+ var decoratedWindow = findDecoratedWindow(dashSurfaceId);
384− verify(dashDelegate);
482+ verify(decoratedWindow);
385
483
484+ tryCompare(dashSurface, "visible", true);
386− findChild(dashDelegate, "decoratedWindow").minimize();
485+ decoratedWindow.minimize();
387− tryCompare(dashApp.session.lastSurface, "visible", false);
486+ tryCompare(dashSurface, "visible", false);
388 }
487 }
389
488
390 function test_maximizeApplicationHidesSurfacesBehindIt() {
489 function test_maximizeApplicationHidesSurfacesBehindIt() {
391− var dashApp = startApplication("unity8-dash");
490+ var dashDelegate = startApplication("unity8-dash");
392− var dialerApp = startApplication("dialer-app");
491+ var dialerDelegate = startApplication("dialer-app");
393− var gmailApp = startApplication("gmail-webapp");
492+ var gmailDelegate = startApplication("gmail-webapp");
394
493
395− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
396− verify(dashDelegate);
397− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
398− verify(dialerDelegate);
399− var gmailDelegate = findChild(desktopStage, "appDelegate_gmail-webapp");
400− verify(gmailDelegate);
401−
402− // maximize
494+ // maximize without raising
403 dialerDelegate.maximize();
495 dialerDelegate.maximize();
404 tryCompare(dialerDelegate, "visuallyMaximized", true);
496 tryCompare(dialerDelegate, "visuallyMaximized", true);
405
497
406− tryCompare(dashApp.session.lastSurface, "visible", false);
498+ tryCompare(dashDelegate.surface, "visible", false);
407− compare(gmailApp.session.lastSurface.visible, true);
499+ compare(gmailDelegate.surface.visible, true);
408
500
409− // restore
501+ // restore without raising
410 dialerDelegate.restoreFromMaximized();
502 dialerDelegate.restoreFromMaximized();
411− compare(dashApp.session.lastSurface.visible, true);
503+ compare(dashDelegate.surface.visible, true);
412− compare(gmailApp.session.lastSurface.visible, true);
504+ compare(gmailDelegate.surface.visible, true);
413 }
505 }
414
506
415 function test_applicationsBecomeVisibleWhenOccludingAppRemoved() {
507 function test_applicationsBecomeVisibleWhenOccludingAppRemoved() {
416− var dashApp = startApplication("unity8-dash");
508+ var dashApp = topSurfaceList.applicationAt(0);
417− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
418− verify(dashDelegate);
419
509
420− var dialerApp = startApplication("dialer-app");
510+ var dialerSurfaceId = topSurfaceList.nextId;
421− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
511+ var dialerDelegate = startApplication("dialer-app");
422 verify(dialerDelegate);
512 verify(dialerDelegate);
513+ var dialerApp = dialerDelegate.application;
423
514
424 var dialerMaximizeButton = findChild(dialerDelegate, "maximizeWindowButton");
515 var dialerMaximizeButton = findChild(dialerDelegate, "maximizeWindowButton");
425 verify(dialerMaximizeButton);
516 verify(dialerMaximizeButton);
426 mouseClick(dialerMaximizeButton);
517 mouseClick(dialerMaximizeButton);
427
518
428− var mapApp = startApplication("map");
519+ var mapSurfaceId = topSurfaceList.nextId;
429− var mapDelegate = findChild(desktopStage, "appDelegate_map");
520+ var mapDelegate = startApplication("map");
430 verify(mapDelegate);
521 verify(mapDelegate);
522+ var mapApp = mapDelegate.application;
431
523
432− var gmailApp = startApplication("gmail-webapp");
524+ var gmailSurfaceId = topSurfaceList.nextId;
433− var gmailDelegate = findChild(desktopStage, "appDelegate_gmail-webapp");
525+ var gmailDelegate = startApplication("gmail-webapp");
434 verify(gmailDelegate);
526 verify(gmailDelegate);
435
527
436 var gmailMaximizeButton = findChild(gmailDelegate, "maximizeWindowButton");
528 var gmailMaximizeButton = findChild(gmailDelegate, "maximizeWindowButton");
@@ -440,29 +532,27 @@ Item {
440 tryCompare(dialerDelegate, "visuallyMaximized", true);
532 tryCompare(dialerDelegate, "visuallyMaximized", true);
441 tryCompare(gmailDelegate, "visuallyMaximized", true);
533 tryCompare(gmailDelegate, "visuallyMaximized", true);
442
534
443− tryCompare(dashApp.session.lastSurface, "visible", false);
535+ tryCompare(dashApp.surfaceList.get(0), "visible", false);
444− tryCompare(dialerApp.session.lastSurface, "visible", false);
536+ tryCompare(dialerApp.surfaceList.get(0), "visible", false);
445− tryCompare(mapApp.session.lastSurface, "visible", false);
537+ tryCompare(mapApp.surfaceList.get(0), "visible", false);
446
538
447 ApplicationManager.stopApplication("gmail-webapp");
539 ApplicationManager.stopApplication("gmail-webapp");
448
540
449− compare(mapApp.session.lastSurface.visible, true);
541+ tryCompare(mapApp.surfaceList.get(0), "visible", true);
450− tryCompare(dialerApp.session.lastSurface, "visible", true);
542+ tryCompare(dialerApp.surfaceList.get(0), "visible", true);
451− tryCompare(dashApp.session.lastSurface, "visible", false); // still occluded by maximised dialer
543+ tryCompare(dashApp.surfaceList.get(0), "visible", false); // still occluded by maximised dialer
452 }
544 }
453
545
454 function test_maximisedAppStaysVisibleWhenAppStarts() {
546 function test_maximisedAppStaysVisibleWhenAppStarts() {
455− var dashApp = startApplication("unity8-dash");
547+ var dashDelegate = startApplication("unity8-dash");
456− var dashDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
548+
457− verify(dashDelegate);
458 // maximize
549 // maximize
459 var dashMaximizeButton = findChild(dashDelegate, "maximizeWindowButton");
550 var dashMaximizeButton = findChild(dashDelegate, "maximizeWindowButton");
460 verify(dashMaximizeButton);
551 verify(dashMaximizeButton);
461 mouseClick(dashMaximizeButton);
552 mouseClick(dashMaximizeButton);
462 tryCompare(dashDelegate, "visuallyMaximized", true);
553 tryCompare(dashDelegate, "visuallyMaximized", true);
463
554
464− var dialerApp = startApplication("dialer-app");
555+ var dialerDelegate = startApplication("dialer-app");
465− var dialerDelegate = findChild(desktopStage, "appDelegate_dialer-app");
466 verify(dialerDelegate);
556 verify(dialerDelegate);
467
557
468 compare(dialerDelegate.visible, true, "Dialer should be visible");
558 compare(dialerDelegate.visible, true, "Dialer should be visible");
@@ -470,14 +560,11 @@ Item {
470 }
560 }
471
561
472 function test_occlusionWithMultipleMaximized() {
562 function test_occlusionWithMultipleMaximized() {
473− var dashApp = startApplication("unity8-dash");
563+ var dashAppDelegate = startApplication("unity8-dash");
474− var dashAppDelegate = findChild(desktopStage, "appDelegate_unity8-dash");
475
564
476− var dialerApp = startApplication("dialer-app");
565+ var dialerAppDelegate = startApplication("dialer-app");
477− var dialerAppDelegate = findChild(desktopStage, "appDelegate_dialer-app");
478
566
479− var facebookApp = startApplication("facebook-webapp");
567+ var facebookAppDelegate = startApplication("facebook-webapp");
480− var facebookAppDelegate = findChild(desktopStage, "appDelegate_facebook-webapp");
481
568
482 // all of them are in restored state now. all should be visible
569 // all of them are in restored state now. all should be visible
483 tryCompare(dashAppDelegate, "visible", true);
570 tryCompare(dashAppDelegate, "visible", true);
@@ -491,13 +578,13 @@ Item {
491 tryCompare(facebookAppDelegate, "visible", true);
578 tryCompare(facebookAppDelegate, "visible", true);
492
579
493 // Bring dash to front. make sure dash and the maximized facebook are visible, the restored one behind is hidden
580 // Bring dash to front. make sure dash and the maximized facebook are visible, the restored one behind is hidden
494− ApplicationManager.focusApplication("unity8-dash");
581+ dashAppDelegate.focus = true;
495 tryCompare(dashAppDelegate, "visible", true);
582 tryCompare(dashAppDelegate, "visible", true);
496 tryCompare(dialerAppDelegate, "visible", false);
583 tryCompare(dialerAppDelegate, "visible", false);
497 tryCompare(facebookAppDelegate, "visible", true);
584 tryCompare(facebookAppDelegate, "visible", true);
498
585
499 // Now focus the dialer app. all 3 should be visible again
586 // Now focus the dialer app. all 3 should be visible again
500− ApplicationManager.focusApplication("dialer-app");
587+ dialerAppDelegate.focus = true;
501 tryCompare(dashAppDelegate, "visible", true);
588 tryCompare(dashAppDelegate, "visible", true);
502 tryCompare(dialerAppDelegate, "visible", true);
589 tryCompare(dialerAppDelegate, "visible", true);
503 tryCompare(facebookAppDelegate, "visible", true);
590 tryCompare(facebookAppDelegate, "visible", true);
@@ -510,20 +597,15 @@ Item {
510 }
597 }
511
598
512 function test_dropShadow() {
599 function test_dropShadow() {
513− // verify the drop shadow is not visible initially
514− verify(PanelState.dropShadow == false);
515−
516 // start an app, maximize it
600 // start an app, maximize it
517− var facebookApp = startApplication("facebook-webapp");
601+ var facebookAppDelegate = startApplication("facebook-webapp");
518− var facebookAppDelegate = findChild(desktopStage, "appDelegate_facebook-webapp");
519 facebookAppDelegate.maximize();
602 facebookAppDelegate.maximize();
520
603
521 // verify the drop shadow is still not visible
604 // verify the drop shadow is still not visible
522 verify(PanelState.dropShadow == false);
605 verify(PanelState.dropShadow == false);
523
606
524 // start a foreground app, not maximized
607 // start a foreground app, not maximized
525− var dialerApp = startApplication("dialer-app");
608+ var dialerAppDelegate = startApplication("dialer-app");
526− var dialerAppDelegate = findChild(desktopStage, "appDelegate_dialer-app");
527
609
528 // verify the drop shadow becomes visible
610 // verify the drop shadow becomes visible
529 verify(PanelState.dropShadow == true);
611 verify(PanelState.dropShadow == true);
@@ -532,7 +614,7 @@ Item {
532 ApplicationManager.stopApplication("facebook-webapp");
614 ApplicationManager.stopApplication("facebook-webapp");
533
615
534 // verify the drop shadow is gone
616 // verify the drop shadow is gone
535− verify(PanelState.dropShadow == false);
617+ tryCompare(PanelState, "dropShadow", false);
536 }
618 }
537 }
619 }
538}
620}
tests/qmltests/Stages/tst_PhoneStage.qml+272−82
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -22,8 +22,10 @@ import "../../../qml/Components"
22import "../../../qml/Stages"
22import "../../../qml/Stages"
23import Ubuntu.Components 1.3
23import Ubuntu.Components 1.3
24import Unity.Application 0.1
24import Unity.Application 0.1
25+import WindowManager 0.1
25
26
26Item {
27Item {
28+ id: root
27 width: units.gu(70)
29 width: units.gu(70)
28 height: units.gu(70)
30 height: units.gu(70)
29
31
@@ -38,63 +40,40 @@ Item {
38 interactive: true
40 interactive: true
39 shellOrientation: Qt.PortraitOrientation
41 shellOrientation: Qt.PortraitOrientation
40 orientations: Orientations {}
42 orientations: Orientations {}
43+ applicationManager: ApplicationManager
44+ topLevelSurfaceList: TopLevelSurfaceList {
45+ id: topLevelSurfaceList
46+ applicationsModel: ApplicationManager
47+ }
41 }
48 }
42
49
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)
43− Rectangle {
57+ Rectangle {
44− anchors { fill: parent; leftMargin: phoneStage.width }
58+ id: controlRect
45−
59+ anchors { left: parent.left; right: parent.right }
60+ height: childrenRect.height + units.gu(2)
61+ color: "darkGrey"
46− Column {
62+ Column {
47− id: buttons
48− anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
63+ anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
49− spacing: units.gu(1)
64+ spacing: units.gu(1)
50− Button {
65+ EdgeBarrierControls {
51− anchors { left: parent.left; right: parent.right }
66+ id: edgeBarrierControls
52− text: "Add App"
67+ text: "Drag here to pull out spread"
53− activeFocusOnPress: false
68+ backgroundColor: "blue"
54− onClicked: {
69+ onDragged: { phoneStage.pushRightEdge(amount); }
55− testCase.addApps();
70+ Component.onCompleted: {
56− }
71+ edgeBarrierControls.target = testCase.findChild(phoneStage, "edgeBarrierController");
57− }
72+ }
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);
72 }
73 }
73− }
74+ Repeater {
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
75+ model: ApplicationManager.availableApplications
90− delegate: Rectangle {
76+ ApplicationCheckBox { appId: modelData }
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; }
98 }
77 }
99 }
78 }
100 }
79 }
@@ -105,18 +84,41 @@ Item {
105 name: "PhoneStage"
84 name: "PhoneStage"
106 when: windowShown
85 when: windowShown
107
86
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+
108 function addApps(count) {
108 function addApps(count) {
109 if (count == undefined) count = 1;
109 if (count == undefined) count = 1;
110 for (var i = 0; i < count; i++) {
110 for (var i = 0; i < count; i++) {
111+ var appSurfaceId = topLevelSurfaceList.nextId;
111 var app = ApplicationManager.startApplication(ApplicationManager.availableApplications[ApplicationManager.count])
112 var app = ApplicationManager.startApplication(ApplicationManager.availableApplications[ApplicationManager.count])
112 tryCompare(app, "state", ApplicationInfoInterface.Running)
113 tryCompare(app, "state", ApplicationInfoInterface.Running)
113 var spreadView = findChild(phoneStage, "spreadView");
114 var spreadView = findChild(phoneStage, "spreadView");
114 tryCompare(spreadView, "contentX", -spreadView.shift);
115 tryCompare(spreadView, "contentX", -spreadView.shift);
116+ waitUntilAppSurfaceShowsUp(appSurfaceId);
115 waitForRendering(phoneStage)
117 waitForRendering(phoneStage)
116 }
118 }
117 }
119 }
118
120
119− function goToSpread() {
121+ function performEdgeSwipeToShowAppSpread() {
120 var spreadView = findChild(phoneStage, "spreadView");
122 var spreadView = findChild(phoneStage, "spreadView");
121
123
122 // Keep it inside the PhoneStage otherwise the controls on the right side will
124 // Keep it inside the PhoneStage otherwise the controls on the right side will
@@ -130,9 +132,46 @@ Item {
130 true /* beginTouch */, true /* endTouch */, units.gu(10), 50);
132 true /* beginTouch */, true /* endTouch */, units.gu(10), 50);
131
133
132 tryCompare(spreadView, "phase", 2);
134 tryCompare(spreadView, "phase", 2);
135+ tryCompare(spreadView, "flicking", false);
136+ tryCompare(spreadView, "moving", false);
133 waitForRendering(phoneStage);
137 waitForRendering(phoneStage);
134 }
138 }
135
139
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+
136 function test_shortFlick() {
175 function test_shortFlick() {
137 addApps(2)
176 addApps(2)
138 var startX = phoneStage.width - units.gu(1);
177 var startX = phoneStage.width - units.gu(1);
@@ -200,7 +239,7 @@ Item {
200 tryCompare(spreadView, "phase", data.targetPhase)
239 tryCompare(spreadView, "phase", data.targetPhase)
201
240
202 if (data.targetPhase == 2) {
241 if (data.targetPhase == 2) {
203− var app2 = findChild(spreadView, "appDelegate2");
242+ var app2 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(2));
204 tryCompare(app2, "swipeToCloseEnabled", true);
243 tryCompare(app2, "swipeToCloseEnabled", true);
205 mouseClick(app2, units.gu(1), units.gu(1));
244 mouseClick(app2, units.gu(1), units.gu(1));
206 }
245 }
@@ -226,11 +265,11 @@ Item {
226
265
227 var spreadView = findChild(phoneStage, "spreadView");
266 var spreadView = findChild(phoneStage, "spreadView");
228
267
229− goToSpread();
268+ performEdgeSwipeToShowAppSpread();
230
269
231 tryCompare(spreadView, "phase", 2);
270 tryCompare(spreadView, "phase", 2);
232
271
233− var tile = findChild(spreadView, "appDelegate" + data.index);
272+ var tile = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(data.index));
234 var appId = ApplicationManager.get(data.index).appId;
273 var appId = ApplicationManager.get(data.index).appId;
235
274
236 if (tile.mapToItem(spreadView).x > spreadView.width) {
275 if (tile.mapToItem(spreadView).x > spreadView.width) {
@@ -266,7 +305,7 @@ Item {
266 var spreadView = findChild(phoneStage, "spreadView");
305 var spreadView = findChild(phoneStage, "spreadView");
267 var selectedApp = ApplicationManager.get(data.index);
306 var selectedApp = ApplicationManager.get(data.index);
268
307
269− goToSpread();
308+ performEdgeSwipeToShowAppSpread();
270
309
271 phoneStage.select(selectedApp.appId);
310 phoneStage.select(selectedApp.appId);
272
311
@@ -280,7 +319,7 @@ Item {
280
319
281 var focusedAppId = ApplicationManager.focusedApplicationId;
320 var focusedAppId = ApplicationManager.focusedApplicationId;
282
321
283− goToSpread();
322+ performEdgeSwipeToShowAppSpread();
284
323
285 mouseClick(phoneStage, units.gu(1), units.gu(1));
324 mouseClick(phoneStage, units.gu(1), units.gu(1));
286
325
@@ -292,15 +331,26 @@ Item {
292 compare(focusedAppId, ApplicationManager.focusedApplicationId);
331 compare(focusedAppId, ApplicationManager.focusedApplicationId);
293 }
332 }
294
333
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+
295 function cleanup() {
343 function cleanup() {
296− while (ApplicationManager.count > 1) {
344+ killApps();
297− var oldCount = ApplicationManager.count;
345+
298− var closingIndex = ApplicationManager.focusedApplicationId == "unity8-dash" ? 1 : 0
299− ApplicationManager.stopApplication(ApplicationManager.get(closingIndex).appId)
300− tryCompare(ApplicationManager, "count", oldCount - 1)
301− }
302 phoneStage.shellOrientationAngle = 0;
346 phoneStage.shellOrientationAngle = 0;
303 phoneStage.select(ApplicationManager.get(0).appId);
347 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+ }
304 }
354 }
305
355
306 function test_focusNewTopMostAppAfterFocusedOneClosesItself() {
356 function test_focusNewTopMostAppAfterFocusedOneClosesItself() {
@@ -323,16 +373,16 @@ Item {
323 function test_cantCloseWhileSnapping() {
373 function test_cantCloseWhileSnapping() {
324 addApps(2);
374 addApps(2);
325
375
326− goToSpread();
376+ performEdgeSwipeToShowAppSpread();
327
377
328 var spreadView = findChild(phoneStage, "spreadView");
378 var spreadView = findChild(phoneStage, "spreadView");
329 var selectedApp = ApplicationManager.get(2);
379 var selectedApp = ApplicationManager.get(2);
330
380
331− goToSpread();
381+ performEdgeSwipeToShowAppSpread();
332
382
333− var app0 = findChild(spreadView, "appDelegate0");
383+ var app0 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
334− var app1 = findChild(spreadView, "appDelegate1");
384+ var app1 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
335− var app2 = findChild(spreadView, "appDelegate2");
385+ var app2 = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(2));
336
386
337 var dragArea0 = findChild(app0, "dragArea");
387 var dragArea0 = findChild(app0, "dragArea");
338 var dragArea1 = findChild(app1, "dragArea");
388 var dragArea1 = findChild(app1, "dragArea");
@@ -382,10 +432,10 @@ Item {
382 addApps(2);
432 addApps(2);
383
433
384 if (data.inSpread) {
434 if (data.inSpread) {
385− goToSpread();
435+ performEdgeSwipeToShowAppSpread();
386 }
436 }
387
437
388− var focusedDelegate = findChild(phoneStage, "appDelegate0");
438+ var focusedDelegate = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
389 phoneStage.inverseProgress = data.leftEdgeDragWidth;
439 phoneStage.inverseProgress = data.leftEdgeDragWidth;
390
440
391 tryCompare(focusedDelegate, "x", data.shouldMoveApp ? data.leftEdgeDragWidth : 0);
441 tryCompare(focusedDelegate, "x", data.shouldMoveApp ? data.leftEdgeDragWidth : 0);
@@ -398,9 +448,9 @@ Item {
398 function test_focusedAppIsTheOnlyRunningApp() {
448 function test_focusedAppIsTheOnlyRunningApp() {
399 addApps(2);
449 addApps(2);
400
450
401− var delegateA = findChild(phoneStage, "appDelegate0");
451+ var delegateA = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
402 verify(delegateA);
452 verify(delegateA);
403− var delegateB = findChild(phoneStage, "appDelegate1");
453+ var delegateB = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
404 verify(delegateB);
454 verify(delegateB);
405
455
406 // A is focused and running, B is unfocused and suspended
456 // A is focused and running, B is unfocused and suspended
@@ -410,7 +460,7 @@ Item {
410 compare(delegateB.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
460 compare(delegateB.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
411
461
412 // Switch foreground/focused appp from A to B
462 // Switch foreground/focused appp from A to B
413− goToSpread();
463+ performEdgeSwipeToShowAppSpread();
414 phoneStage.select(delegateB.application.appId);
464 phoneStage.select(delegateB.application.appId);
415
465
416 // Now it's the other way round
466 // Now it's the other way round
@@ -424,14 +474,14 @@ Item {
424 function test_dashRemainsRunningIfStageIsToldSo() {
474 function test_dashRemainsRunningIfStageIsToldSo() {
425 addApps(1);
475 addApps(1);
426
476
427− var delegateDash = findChild(phoneStage, "appDelegate1");
477+ var delegateDash = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
428 verify(delegateDash);
478 verify(delegateDash);
429 compare(delegateDash.application.appId, "unity8-dash");
479 compare(delegateDash.application.appId, "unity8-dash");
430
480
431− var delegateOther = findChild(phoneStage, "appDelegate0");
481+ var delegateOther = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
432 verify(delegateOther);
482 verify(delegateOther);
433
483
434− goToSpread();
484+ performEdgeSwipeToShowAppSpread();
435 phoneStage.select("unity8-dash");
485 phoneStage.select("unity8-dash");
436
486
437 tryCompare(delegateDash, "focus", true);
487 tryCompare(delegateDash, "focus", true);
@@ -439,7 +489,7 @@ Item {
439 compare(delegateOther.focus, false);
489 compare(delegateOther.focus, false);
440 compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
490 compare(delegateOther.application.requestedState, ApplicationInfoInterface.RequestedSuspended);
441
491
442− goToSpread();
492+ performEdgeSwipeToShowAppSpread();
443 phoneStage.select(delegateOther.application.appId);
493 phoneStage.select(delegateOther.application.appId);
444
494
445 // The other app gets focused and running but dash is kept running despite being unfocused
495 // The other app gets focused and running but dash is kept running despite being unfocused
@@ -452,7 +502,7 @@ Item {
452 function test_foregroundAppIsSuspendedWhenStageIsSuspended() {
502 function test_foregroundAppIsSuspendedWhenStageIsSuspended() {
453 addApps(1);
503 addApps(1);
454
504
455− var delegate = findChild(phoneStage, "appDelegate0");
505+ var delegate = findChild(phoneStage, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
456 verify(delegate);
506 verify(delegate);
457
507
458 compare(delegate.focus, true);
508 compare(delegate.focus, true);
@@ -476,5 +526,145 @@ Item {
476 }
526 }
477 tryCompare(spreadView, "phase", 2);
527 tryCompare(spreadView, "phase", 2);
478 }
528 }
529+
530+ function test_closeSurfaceOfMultiSurfaceApp() {
531+ var surface1Id = topLevelSurfaceList.nextId;
532+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
533+ waitUntilAppSurfaceShowsUp(surface1Id);
534+
535+ var surface2Id = topLevelSurfaceList.nextId;
536+ verify(surface1Id !== surface2Id); // sanity checking
537+ webbrowserApp.createSurface();
538+ waitUntilAppSurfaceShowsUp(surface2Id);
539+
540+ performEdgeSwipeToShowAppSpread();
541+
542+ var appDelegate = findChild(phoneStage, "spreadDelegate_" + surface1Id);
543+ verify(appDelegate);
544+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
545+
546+ compare(webbrowserApp.surfaceList.count, 2);
547+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
548+
549+ swipeSurfaceUpwards(surface1Id);
550+
551+ // Surface must eventually be gone
552+ tryCompareFunction(function() { return topLevelSurfaceList.indexForId(surface1Id); }, -1);
553+ tryCompare(webbrowserApp.surfaceList, "count", 1);
554+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
555+ }
556+
557+ /*
558+ 1- Suspended app gets killed behind the scenes, causing its surface to go zombie.
559+ 2- Surface gets screenshotted and removed. Its slot in the topLevelSurfaceList remains,
560+ though (so ApplicationWindow can display the screenshot in its place).
561+ 3- User taps on the screenshot of the long-gone surface.
562+
563+ Expected outcome:
564+ Application gets relaunched. Its new surface will seamlessly replace the screenshot.
565+ */
566+ function test_selectSuspendedAppWithoutSurface() {
567+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
568+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
569+ var dashSurface = topLevelSurfaceList.surfaceAt(0);
570+
571+ var webbrowserSurfaceId = topLevelSurfaceList.nextId;
572+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
573+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
574+
575+ switchToSurface(dashSurfaceId);
576+
577+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
578+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
579+
580+ compare(webbrowserApp.surfaceList.count, 1);
581+
582+ // simulate the suspended app being killed by the out-of-memory daemon
583+ webbrowserApp.surfaceList.get(0).setLive(false);
584+
585+ // wait until the surface is gone
586+ tryCompare(webbrowserApp.surfaceList, "count", 0);
587+
588+ compare(topLevelSurfaceList.surfaceAt(topLevelSurfaceList.indexForId(webbrowserSurfaceId)), null);
589+
590+ switchToSurface(webbrowserSurfaceId);
591+
592+ // webbrowser should have been brought to front
593+ tryCompareFunction(function(){return topLevelSurfaceList.idAt(0);}, webbrowserSurfaceId);
594+
595+ // and it should eventually get a new surface and get resumed
596+ tryCompareFunction(function(){return topLevelSurfaceList.surfaceAt(0) !== null;}, true);
597+ compare(topLevelSurfaceList.count, 2); // still two top-level items
598+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
599+ compare(webbrowserApp.surfaceList.count, 1);
600+ }
601+
602+ /*
603+ 1 - user suspends an application (ie, focus a surface from a different application)
604+ 2 - That suspended application gets killed, causing its surface to go zombie
605+ 3 - user goes to spread and closes that zombie surface. Actually, by that time
606+ Tablet shell will be displaying a screenshot of it instead, so there's no
607+ MirSurface whatsoever backing it up.
608+ */
609+ function test_closeZombieSurface()
610+ {
611+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
612+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
613+ var dashSurface = topLevelSurfaceList.surfaceAt(0);
614+
615+ var webbrowserSurfaceId = topLevelSurfaceList.nextId;
616+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
617+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
618+
619+ switchToSurface(dashSurfaceId);
620+
621+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
622+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
623+
624+ compare(webbrowserApp.surfaceList.count, 1);
625+
626+ // simulate the suspended app being killed by the out-of-memory daemon
627+ webbrowserApp.surfaceList.get(0).setLive(false);
628+
629+ // wait until the surface is gone
630+ tryCompare(webbrowserApp.surfaceList, "count", 0);
631+ compare(topLevelSurfaceList.surfaceAt(topLevelSurfaceList.indexForId(webbrowserSurfaceId)), null);
632+
633+ performEdgeSwipeToShowAppSpread();
634+
635+ {
636+ var appDelegate = findChild(phoneStage, "spreadDelegate_" + webbrowserSurfaceId);
637+ verify(appDelegate);
638+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
639+ }
640+
641+ swipeSurfaceUpwards(webbrowserSurfaceId);
642+
643+ // webbrowser entry is nowhere to be seen
644+ tryCompareFunction(function(){return topLevelSurfaceList.indexForId(webbrowserSurfaceId);}, -1);
645+
646+ // nor is its app
647+ compare(ApplicationManager.findApplication("webbrowser-app"), null);
648+
649+ // only unity8-dash surface is left
650+ compare(topLevelSurfaceList.count, 1);
651+ }
652+
653+
654+ /*
655+ Check that when an application starts while the spread is open, the
656+ spread closes and that new app is brought to front (gets focused).
657+ */
658+ function test_launchAppWithSpreadOpen()
659+ {
660+ performEdgeSwipeToShowAppSpread();
661+
662+ var webbrowserSurfaceId = topLevelSurfaceList.nextId;
663+ var webbrowserApp = ApplicationManager.startApplication("webbrowser-app");
664+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
665+
666+ compare(topLevelSurfaceList.idAt(0), webbrowserSurfaceId);
667+ compare(webbrowserApp.focused, true);
668+ }
479 }
669 }
480}
670}
tests/qmltests/Stages/tst_SessionContainer.qmldeleted−331
@@ -1,331 +0,0 @@
1−/*
2− * Copyright 2014-2015 Canonical Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.4
18−import QtQuick.Layouts 1.1
19−import QtTest 1.0
20−import Unity.Test 0.1 as UT
21−import Ubuntu.Components 1.3
22−import Unity.Application 0.1
23−import "../../../qml/Stages"
24−
25−Rectangle {
26− color: "red"
27− id: root
28− width: units.gu(80)
29− height: units.gu(70)
30−
31− Connections {
32− target: sessionContainerLoader.status === Loader.Ready ? sessionContainerLoader.item : null
33− onSessionChanged: {
34− sessionCheckbox.checked = sessionContainerLoader.item.session !== null
35− }
36− }
37−
38− Component {
39− id: sessionContainerComponent
40−
41− SessionContainer {
42− id: sessionContainer
43− anchors.fill: parent
44− focus: true
45− interactive: true
46− }
47− }
48−
49− Loader {
50− id: sessionContainerLoader
51− focus: true
52− anchors {
53− top: parent.top
54− bottom: parent.bottom
55− left: parent.left
56− }
57− width: units.gu(40)
58− sourceComponent: sessionContainerComponent
59− }
60−
61− Rectangle {
62− color: "white"
63− anchors {
64− top: parent.top
65− bottom: parent.bottom
66− left: sessionContainerLoader.right
67− right: parent.right
68− }
69−
70− ColumnLayout {
71− anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
72− spacing: units.gu(1)
73−
74− RowLayout {
75− Layout.fillWidth: true
76−
77− CheckBox {
78− id: sessionCheckbox;
79− checked: false;
80− activeFocusOnPress: false
81− onCheckedChanged: {
82− if (sessionContainerLoader.status !== Loader.Ready)
83− return;
84−
85− if (checked) {
86− var fakeSession = SessionManager.createSession("music-player", Qt.resolvedUrl("../Dash/artwork/music-player-design.png"));
87− sessionContainerLoader.item.session = fakeSession;
88− fakeSession.createSurface();
89− } else {
90− sessionContainerLoader.item.session.release();
91− }
92− }
93− }
94−
95− Label {
96− text: "session"
97− anchors.verticalCenter: parent.verticalCenter
98− }
99− }
100−
101− Rectangle {
102− border {
103− color: "black"
104− width: 1
105− }
106− anchors {
107− left: parent.left
108− right: parent.right
109− }
110− Layout.preferredHeight: sessionChildrenControl.height
111−
112− RecursingChildSessionControl {
113− id: sessionChildrenControl
114− anchors { left: parent.left; right: parent.right; }
115−
116− session: sessionContainerLoader.item ? sessionContainerLoader.item.session : null
117− }
118− }
119− }
120− }
121−
122− SignalSpy {
123− id: sessionSpy
124− target: SessionManager
125− signalName: "sessionStopping"
126− }
127−
128− UT.UnityTestCase {
129− id: testCase
130− name: "SessionContainer"
131−
132− function init() {
133− // reload our test subject to get it in a fresh state once again
134− sessionContainerLoader.active = false;
135− sessionCheckbox.checked = false;
136− sessionContainerLoader.active = true;
137−
138− tryCompare(sessionContainerLoader.item, "session", null);
139− sessionSpy.clear();
140− }
141−
142− when: windowShown
143−
144− function test_addChildSession_data() {
145− return [ { tag: "count=1", count: 1 },
146− { tag: "count=4", count: 4 } ];
147− }
148−
149− function test_addChildSession(data) {
150− sessionCheckbox.checked = true;
151− var sessionContainer = sessionContainerLoader.item;
152− compare(sessionContainer.childSessions.count(), 0);
153−
154− var i;
155− var sessions = [];
156− for (i = 0; i < data.count; i++) {
157− var session = ApplicationTest.addChildSession(sessionContainer.session,
158− "gallery",
159− true);
160− session.createSurface();
161− sessionContainer.session.addChildSession(session);
162− compare(sessionContainer.childSessions.count(), i+1);
163−
164− sessions.push(session);
165− }
166−
167− for (i = data.count-1; i >= 0; i--) {
168− ApplicationTest.removeSession(sessions[i]);
169− tryCompareFunction(function() { return sessionContainer.childSessions.count(); }, i);
170− }
171− tryCompare(sessionSpy, "count", data.count);
172− }
173−
174− function test_childSessionDestructionReturnsFocusToSiblingOrParent() {
175− sessionCheckbox.checked = true;
176− var sessionContainer = sessionContainerLoader.item;
177− compare(sessionContainer.childSessions.count(), 0);
178−
179− var i;
180− var sessions = [];
181− // 3 sessions should cover all edge cases
182− for(i = 0; i < 3; i++) {
183− var a_session = ApplicationTest.addChildSession(
184− sessionContainer.session, "gallery", true
185− )
186−
187− a_session.createSurface();
188− sessionContainer.session.addChildSession(a_session);
189− compare(sessionContainer.childSessions.count(), i + 1);
190−
191− sessions.push(a_session);
192− }
193−
194− var a_session;
195− while(a_session = sessions.pop()) {
196− compare(a_session.lastSurface.activeFocus, true);
197−
198− ApplicationTest.removeSurface(a_session.lastSurface);
199− ApplicationTest.removeSession(a_session);
200−
201− if (sessions.length > 0) {
202− // active focus should have gone to the yongest remaining sibling
203− var previousSiblingSurface = sessions[sessions.length - 1].lastSurface
204− tryCompare(previousSiblingSurface, "activeFocus", true);
205− } else {
206− // active focus should have gone to the parent surface
207− tryCompare(sessionContainer.session.lastSurface, "activeFocus", true);
208− }
209− }
210− }
211−
212− function test_nestedChildSessions_data() {
213− return [ { tag: "depth=2", depth: 2 },
214− { tag: "depth=8", depth: 8 }
215− ];
216− }
217−
218− function test_nestedChildSessions(data) {
219− sessionCheckbox.checked = true;
220− var sessionContainer = sessionContainerLoader.item;
221− compare(sessionContainer.childSessions.count(), 0);
222−
223− var i;
224− var sessions = [];
225− var lastSession = sessionContainer.session;
226− var delegate;
227− var container = sessionContainer;
228− for (i = 0; i < data.depth; i++) {
229− var session = ApplicationTest.addChildSession(lastSession,
230− "gallery",
231− true);
232− session.createSurface();
233− lastSession.addChildSession(session);
234− compare(container.childSessions.count(), 1);
235− sessions.push(session);
236−
237− delegate = findChild(container, "childDelegate0");
238− container = findChild(delegate, "sessionContainer");
239− lastSession = session;
240− }
241−
242− for (i = data.depth-1; i >= 0; i--) {
243− ApplicationTest.removeSession(sessions[i]);
244− }
245−
246− tryCompareFunction(function() { return sessionContainer.childSessions.count(); }, 0);
247− tryCompare(sessionSpy, "count", data.depth);
248− }
249−
250− function test_childrenAdjustForParentSize() {
251− sessionCheckbox.checked = true;
252− var sessionContainer = sessionContainerLoader.item;
253−
254− var session = ApplicationTest.addChildSession(sessionContainer.session,
255− "gallery",
256− true);
257− session.createSurface();
258− sessionContainer.session.addChildSession(session);
259−
260− var delegate = findChild(sessionContainer, "childDelegate0");
261− var childContainer = findChild(delegate, "sessionContainer");
262−
263− tryCompareFunction(function() { return childContainer.height === sessionContainer.height; }, true);
264− tryCompareFunction(function() { return childContainer.width === sessionContainer.width; }, true);
265− tryCompareFunction(function() { return childContainer.x === 0; }, true);
266− tryCompareFunction(function() { return childContainer.y === 0; }, true);
267−
268− sessionContainer.anchors.margins = units.gu(2);
269−
270− tryCompareFunction(function() { return childContainer.height === sessionContainer.height; }, true);
271− tryCompareFunction(function() { return childContainer.width === sessionContainer.width; }, true);
272− tryCompareFunction(function() { return childContainer.x === 0; }, true);
273− tryCompareFunction(function() { return childContainer.y === 0; }, true);
274− }
275−
276− function isContainerAnimating(container) {
277− var animation = findInvisibleChild(container, "sessionAnimation");
278− if (!animation) return false;
279−
280− var animating = false;
281− for (var i = 0; i < animation.transitions.length; ++i) {
282− if (animation.transitions[i].running) {
283− return true;
284− }
285− }
286− return false;
287− }
288−
289− function test_childrenAnimate() {
290− sessionCheckbox.checked = true;
291− var sessionContainer = sessionContainerLoader.item;
292−
293− var session = ApplicationTest.addChildSession(sessionContainer.session,
294− "gallery",
295− true);
296−
297− var delegate = findChild(sessionContainer, "childDelegate0");
298− var childContainer = findChild(delegate, "sessionContainer");
299−
300− // wait for animation to begin
301− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
302− // wait for animation to end
303− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
304−
305− ApplicationTest.removeSession(session);
306−
307− // wait for animation to begin
308− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
309− // wait for animation to end
310− tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
311−
312− tryCompareFunction(function() { return sessionContainer.childSessions.count() }, 0);
313− }
314−
315− function test_childSessionsRemovedOnStopWhenNotLive() {
316− sessionCheckbox.checked = true;
317− var sessionContainer = sessionContainerLoader.item;
318−
319− var session = ApplicationTest.addChildSession(sessionContainer.session,
320− "gallery",
321− false);
322−
323− var delegate = findChild(sessionContainer, "childDelegate0");
324− var childContainer = findChild(delegate, "sessionContainer");
325−
326− ApplicationTest.removeSession(session);
327−
328− tryCompareFunction(function() { return sessionContainer.childSessions.count() }, 0);
329− }
330− }
331−}
tests/qmltests/Stages/tst_SpreadDelegate.qml+3−2
@@ -61,6 +61,7 @@ Rectangle {
61 swipeToCloseEnabled: swipeToCloseCheckbox.checked
61 swipeToCloseEnabled: swipeToCloseCheckbox.checked
62 closeable: closeableCheckbox.checked
62 closeable: closeableCheckbox.checked
63 application: fakeApplication
63 application: fakeApplication
64+ surface: fakeApplication && fakeApplication.surfaceList.count > 0 ? fakeApplication.surfaceList.get(0) : null
64 shellOrientationAngle: shellOrientationAngleSelector.value
65 shellOrientationAngle: shellOrientationAngleSelector.value
65 shellOrientation: {
66 shellOrientation: {
66 switch (shellOrientationAngleSelector.value) {
67 switch (shellOrientationAngleSelector.value) {
@@ -162,8 +163,8 @@ Rectangle {
162 unloadSpreadDelegate();
163 unloadSpreadDelegate();
163 spyClosedSignal.clear();
164 spyClosedSignal.clear();
164 shellOrientationAngleSelector.selectedIndex = 0;
165 shellOrientationAngleSelector.selectedIndex = 0;
165− ApplicationManager.stopApplication(root.fakeApplication.appId);
166 root.fakeApplication = null;
166 root.fakeApplication = null;
167+ killApps();
167 }
168 }
168
169
169 function restartWithApp(appId) {
170 function restartWithApp(appId) {
@@ -258,7 +259,7 @@ Rectangle {
258 function test_loadingLandscapeOnlyAppWhenShellInPortrait() {
259 function test_loadingLandscapeOnlyAppWhenShellInPortrait() {
259 loadWithWeatherApp.clicked();
260 loadWithWeatherApp.clicked();
260
261
261− var appWindow = findChild(spreadDelegate, "appWindow_ubuntu-weather-app");
262+ var appWindow = findChild(spreadDelegate, "appWindow");
262 verify(appWindow);
263 verify(appWindow);
263
264
264 // It must have landscape dimensions as it does not support portrait
265 // It must have landscape dimensions as it does not support portrait
tests/qmltests/Stages/tst_SurfaceContainer.qml+242−46
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014-2015 Canonical Ltd.
2+ * Copyright 2014-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -15,28 +15,42 @@
15 */
15 */
16
16
17import QtQuick 2.4
17import QtQuick 2.4
18+import QtQuick.Layouts 1.1
18import QtTest 1.0
19import QtTest 1.0
19−import Unity.Test 0.1
20+import Unity.Test 0.1 as UT
20−import ".."
21−import "../../../qml/Stages"
22import Ubuntu.Components 1.3
21import Ubuntu.Components 1.3
23import Unity.Application 0.1
22import Unity.Application 0.1
23+import "../../../qml/Stages"
24
24
25Rectangle {
25Rectangle {
26− color: "black"
26+ color: "red"
27 id: root
27 id: root
28− width: units.gu(70)
28+ width: units.gu(80)
29 height: units.gu(70)
29 height: units.gu(70)
30
30
31+ Connections {
32+ target: surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
33+ onSurfaceChanged: {
34+ surfaceCheckbox.checked = surfaceContainerLoader.item.surface !== null
35+ }
36+ }
37+
31 Component {
38 Component {
32 id: surfaceContainerComponent
39 id: surfaceContainerComponent
40+
33 SurfaceContainer {
41 SurfaceContainer {
34 anchors.fill: parent
42 anchors.fill: parent
43+ focus: true
35 interactive: interactiveCheckbox.checked
44 interactive: interactiveCheckbox.checked
45+ Component.onDestruction: {
46+ surfaceContainerLoader.itemDestroyed = true;
47+ }
36 }
48 }
37 }
49 }
50+
38 Loader {
51 Loader {
39 id: surfaceContainerLoader
52 id: surfaceContainerLoader
53+ focus: true
40 anchors {
54 anchors {
41 top: parent.top
55 top: parent.top
42 topMargin: fullscreenCheckbox.checked ? 0 : units.gu(3)
56 topMargin: fullscreenCheckbox.checked ? 0 : units.gu(3)
@@ -44,6 +58,7 @@ Rectangle {
44 left: parent.left
58 left: parent.left
45 }
59 }
46 width: units.gu(40)
60 width: units.gu(40)
61+ property bool itemDestroyed: false
47 sourceComponent: surfaceContainerComponent
62 sourceComponent: surfaceContainerComponent
48 }
63 }
49
64
@@ -56,80 +71,257 @@ Rectangle {
56 right: parent.right
71 right: parent.right
57 }
72 }
58
73
59− Column {
74+ ColumnLayout {
60 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
75 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
61 spacing: units.gu(1)
76 spacing: units.gu(1)
77+
78+ RowLayout {
79+ Layout.fillWidth: true
80+
62− Row {
81+ Row {
63− anchors { left: parent.left; right: parent.right }
82+ CheckBox {id: fullscreenCheckbox; checked: true; activeFocusOnPress: false }
83+ Label { text: "fullscreen" }
84+ }
85+ Row {
86+ CheckBox {id: interactiveCheckbox; checked: true; activeFocusOnPress: false }
87+ Label { text: "interactive" }
88+ }
89+ }
90+
91+ RowLayout {
92+ Layout.fillWidth: true
93+
64 CheckBox {
94 CheckBox {
65 id: surfaceCheckbox;
95 id: surfaceCheckbox;
66 checked: false;
96 checked: false;
97+ activeFocusOnPress: false
67 onCheckedChanged: {
98 onCheckedChanged: {
68 if (surfaceContainerLoader.status !== Loader.Ready)
99 if (surfaceContainerLoader.status !== Loader.Ready)
69 return;
100 return;
70
101
71 if (checked) {
102 if (checked) {
103+ var application = ApplicationManager.add("music-app");
104+ application.manualSurfaceCreation = true;
105+ application.setState(ApplicationInfoInterface.Starting);
106+
72− var fakeSurface = SurfaceManager.createSurface("fake-surface",
107+ surfaceContainerLoader.item.surface = SurfaceManager.createSurface("foo",
73− Mir.NormalType,
108+ Mir.NormalType, Mir.MaximizedState, application.screenshot);
74− Mir.RestoredState,
109+
75− Qt.resolvedUrl("../Dash/artwork/music-player-design.png"));
110+ application.setState(ApplicationInfoInterface.Running);
76− surfaceContainerLoader.item.surface = fakeSurface;
77 } else {
111 } else {
112+ if (surfaceContainerLoader.item.surface) {
78− surfaceContainerLoader.item.surface.setLive(false);
113+ surfaceContainerLoader.item.surface.setLive(false);
114+ }
115+ ApplicationManager.stopApplication("music-app");
79 }
116 }
80 }
117 }
81 }
118 }
119+
120+ Label {
82− Label { text: "surface" }
121+ text: "surface"
83− }
122+ anchors.verticalCenter: parent.verticalCenter
84− Row {
123+ }
85− anchors { left: parent.left; right: parent.right }
86− CheckBox {id: fullscreenCheckbox; checked: true; }
87− Label { text: "fullscreen" }
88 }
124 }
89− Row {
125+
126+ Rectangle {
127+ border {
128+ color: "black"
129+ width: 1
130+ }
131+ anchors {
132+ left: parent.left
90− anchors { left: parent.left; right: parent.right }
133+ right: parent.right
91− CheckBox {id: interactiveCheckbox; checked: true; }
134+ }
92− Label { text: "interactive" }
135+ Layout.preferredHeight: surfaceChildrenControl.height
136+
137+ RecursingChildSessionControl {
138+ id: surfaceChildrenControl
139+ anchors { left: parent.left; right: parent.right; }
140+
141+ surface: surfaceContainerLoader.item ? surfaceContainerLoader.item.surface : null
142+ }
93 }
143 }
94− MouseTouchEmulationCheckbox {}
95 }
144 }
96 }
145 }
97
146
98− SignalSpy {
99− id: surfaceSpy
100− target: SurfaceManager
101− signalName: "surfaceDestroyed"
102− }
103−
104− UnityTestCase {
147+ UT.UnityTestCase {
105 id: testCase
148 id: testCase
106 name: "SurfaceContainer"
149 name: "SurfaceContainer"
107− when: windowShown
108
150
109− property var surfaceContainer:
110− surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
151+ property Item surfaceContainer: surfaceContainerLoader.status === Loader.Ready ? surfaceContainerLoader.item : null
111
152
112 function cleanup() {
153 function cleanup() {
113 // reload our test subject to get it in a fresh state once again
154 // reload our test subject to get it in a fresh state once again
155+
156+ surfaceContainerLoader.itemDestroyed = false;
114 surfaceContainerLoader.active = false;
157 surfaceContainerLoader.active = false;
158+
159+ tryCompare(surfaceContainerLoader, "status", Loader.Null);
160+ tryCompare(surfaceContainerLoader, "item", null);
161+ tryCompare(surfaceContainerLoader, "itemDestroyed", true);
162+
163+ killApps();
164+
115 surfaceCheckbox.checked = false;
165 surfaceCheckbox.checked = false;
166+ interactiveCheckbox.checked = true;
167+
116 surfaceContainerLoader.active = true;
168 surfaceContainerLoader.active = true;
169+ tryCompare(surfaceContainerLoader, "status", Loader.Ready);
170+ }
117
171
118− tryCompare(surfaceContainerLoader.item, "surface", null);
172+ when: windowShown
119− surfaceSpy.clear();
120
173
121− interactiveCheckbox.checked = true;
174+ function test_addChildSession_data() {
175+ return [ { tag: "count=1", count: 1 },
176+ { tag: "count=4", count: 4 } ];
122 }
177 }
123
178
124− function test_animateRemoval() {
179+ function test_addChildSession(data) {
125 surfaceCheckbox.checked = true;
180 surfaceCheckbox.checked = true;
181+ var childSurfaces = testCase.findChild(surfaceContainer, "childSurfacesRepeater");
182+ compare(childSurfaces.count, 0);
126
183
184+ var i;
185+ for (i = 0; i < data.count; i++) {
127− verify(surfaceContainer.surface !== null);
186+ surfaceContainer.surface.createPromptSurface();
187+ compare(childSurfaces.count, i+1);
188+ }
189+
190+ for (i = data.count-1; i >= 0; i--) {
191+ {
192+ var childPromptSurface = surfaceContainer.surface.promptSurfaceList.get(i);
193+ childPromptSurface.close();
194+ }
195+ tryCompareFunction(function() { return childSurfaces.count; }, i);
196+ }
197+ }
198+
199+ function test_childSessionDestructionReturnsFocusToSiblingOrParent() {
200+ surfaceCheckbox.checked = true;
201+ var childSurfaces = testCase.findChild(surfaceContainer, "childSurfacesRepeater");
202+ compare(childSurfaces.count, 0);
203+
204+ var i;
205+ // 3 surfaces should cover all edge cases
206+ for (i = 0; i < 3; i++) {
207+ surfaceContainer.surface.createPromptSurface();
208+ compare(childSurfaces.count, i+1);
209+ }
128
210
211+ for (i = 2; i >= 0; --i) {
129− surfaceContainer.surface.setLive(false);
212+ var childPromptSurface = surfaceContainer.surface.promptSurfaceList.get(i);
213+ compare(childPromptSurface.activeFocus, true);
214+
215+ childPromptSurface.close();
216+ tryCompareFunction(function() { return childSurfaces.count; }, i);
217+
218+ if (i > 0) {
219+ // active focus should have gone to the yongest remaining sibling
220+ var previousSiblingSurface = surfaceContainer.surface.promptSurfaceList.get(i-1);
221+ tryCompare(previousSiblingSurface, "activeFocus", true);
222+ } else {
223+ // active focus should have gone to the parent surface
224+ tryCompare(surfaceContainer.surface, "activeFocus", true);
225+ }
226+ }
227+ }
130
228
131− compare(surfaceContainer.state, "zombie");
229+ function test_nestedChildSessions_data() {
132− tryCompare(surfaceContainer, "surface", null);
230+ return [ { tag: "depth=2", depth: 2 },
231+ { tag: "depth=8", depth: 8 }
232+ ];
233+ }
234+ function test_nestedChildSessions(data) {
235+ surfaceCheckbox.checked = true;
236+
237+ var i;
238+ var container = surfaceContainer;
239+ var surface = container.surface;
240+ var surfaces = [surface];
241+ var parent_childSurfaces = [null];
242+ for (i = 0; i < data.depth; i++) {
243+ surface.createPromptSurface();
244+ var childSurfaces = testCase.findChild(container, "childSurfacesRepeater");
245+ compare(childSurfaces.count, 1);
246+
247+ var childDelegate = childSurfaces.itemAt(0);
248+ container = findChild(childDelegate, "surfaceContainer");
249+ {
250+ var animationsLoader = findChild(container, "animationsLoader");
251+ tryCompare(animationsLoader, "status", Loader.Ready);
252+ waitUntilTransitionsEnd(animationsLoader.item);
253+ }
254+ surface = container.surface;
255+
256+ surfaces.push(surface);
257+ parent_childSurfaces.push(childSurfaces);
258+ }
259+
260+ for (i = surfaces.length-1; i >= 0; i--) {
261+ surfaces[i].close();
262+ if (parent_childSurfaces[i]) {
263+ tryCompareFunction(function() { return parent_childSurfaces[i].count; }, 0);
264+ }
265+ }
266+ }
267+
268+ function test_childrenAdjustForParentSize() {
269+ surfaceCheckbox.checked = true;
270+
271+ surfaceContainer.surface.createPromptSurface();
272+
273+ var delegate = findChild(surfaceContainer, "childDelegate0");
274+ var childContainer = findChild(delegate, "surfaceContainer");
275+
276+ tryCompareFunction(function() { return childContainer.height === surfaceContainer.height; }, true);
277+ tryCompareFunction(function() { return childContainer.width === surfaceContainer.width; }, true);
278+ tryCompareFunction(function() { return childContainer.x === 0; }, true);
279+ tryCompareFunction(function() { return childContainer.y === 0; }, true);
280+
281+ surfaceContainer.anchors.margins = units.gu(2);
282+
283+ tryCompareFunction(function() { return childContainer.height === surfaceContainer.height; }, true);
284+ tryCompareFunction(function() { return childContainer.width === surfaceContainer.width; }, true);
285+ tryCompareFunction(function() { return childContainer.x === 0; }, true);
286+ tryCompareFunction(function() { return childContainer.y === 0; }, true);
287+ }
288+
289+ function isContainerAnimating(container) {
290+ var animationsLoader = findChild(container, "animationsLoader");
291+ if (!animationsLoader || animationsLoader.status != Loader.Ready) {
292+ return false;
293+ }
294+
295+ var animation = animationsLoader.item;
296+
297+ var animating = false;
298+ for (var i = 0; i < animation.transitions.length; ++i) {
299+ if (animation.transitions[i].running) {
300+ return true;
301+ }
302+ }
303+ return false;
304+ }
305+
306+ function test_childrenAnimate() {
307+ surfaceCheckbox.checked = true;
308+
309+ surfaceContainer.surface.createPromptSurface();
310+
311+ var delegate = findChild(surfaceContainer, "childDelegate0");
312+ var childContainer = findChild(delegate, "surfaceContainer");
313+
314+ // wait for animation to begin
315+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
316+ // wait for animation to end
317+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
318+
319+ surfaceContainer.surface.promptSurfaceList.get(0).close();
320+
321+ // wait for animation to begin
322+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, true);
323+ // wait for animation to end
324+ tryCompareFunction(function() { return isContainerAnimating(childContainer); }, false);
133 }
325 }
134
326
135 function test_surfaceItemGetsNoTouchesWhenContainerNotInteractive() {
327 function test_surfaceItemGetsNoTouchesWhenContainerNotInteractive() {
@@ -142,14 +334,14 @@ Rectangle {
142 surfaceItem.touchPressCount = 0;
334 surfaceItem.touchPressCount = 0;
143 surfaceItem.touchReleaseCount = 0;
335 surfaceItem.touchReleaseCount = 0;
144
336
145− tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
337+ tap(surfaceContainer);
146
338
147 // surface got touches as the surfaceContainer is interactive
339 // surface got touches as the surfaceContainer is interactive
148 compare(surfaceItem.touchPressCount, 1)
340 compare(surfaceItem.touchPressCount, 1)
149 compare(surfaceItem.touchReleaseCount, 1);
341 compare(surfaceItem.touchReleaseCount, 1);
150
342
151 interactiveCheckbox.checked = false;
343 interactiveCheckbox.checked = false;
152− tap(surfaceContainer, surfaceContainer.width / 2, surfaceContainer.height / 2);
344+ tap(surfaceContainer);
153
345
154 // surface shouldn't get the touches from the second tap as the surfaceContainer
346 // surface shouldn't get the touches from the second tap as the surfaceContainer
155 // was *not* interactive when it happened.
347 // was *not* interactive when it happened.
@@ -164,6 +356,8 @@ Rectangle {
164 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
356 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
165 verify(surfaceItem !== null);
357 verify(surfaceItem !== null);
166
358
359+ surfaceContainer.focus = false;
360+
167 compare(surfaceItem.activeFocus, false);
361 compare(surfaceItem.activeFocus, false);
168 mouseClick(surfaceContainer);
362 mouseClick(surfaceContainer);
169 compare(surfaceItem.activeFocus, true);
363 compare(surfaceItem.activeFocus, true);
@@ -176,6 +370,8 @@ Rectangle {
176 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
370 var surfaceItem = findChild(surfaceContainer, "surfaceItem");
177 verify(surfaceItem !== null);
371 verify(surfaceItem !== null);
178
372
373+ surfaceContainer.focus = false;
374+
179 compare(surfaceItem.activeFocus, false);
375 compare(surfaceItem.activeFocus, false);
180 tap(surfaceContainer);
376 tap(surfaceContainer);
181 compare(surfaceItem.activeFocus, true);
377 compare(surfaceItem.activeFocus, true);
tests/qmltests/Stages/tst_TabletStage.qml+252−89
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@ import Ubuntu.Components.ListItems 1.3 as ListItem
21import Unity.Application 0.1
21import Unity.Application 0.1
22import Unity.Test 0.1
22import Unity.Test 0.1
23import Utils 0.1
23import Utils 0.1
24+import WindowManager 0.1
24
25
25import ".."
26import ".."
26import "../../../qml/Stages"
27import "../../../qml/Stages"
@@ -34,6 +35,11 @@ Rectangle {
34
35
35 property var greeter: { fullyShown: true }
36 property var greeter: { fullyShown: true }
36
37
38+ TopLevelSurfaceList {
39+ id: topSurfaceList
40+ applicationsModel: ApplicationManager
41+ }
42+
37 Loader {
43 Loader {
38 id: tabletStageLoader
44 id: tabletStageLoader
39
45
@@ -62,6 +68,8 @@ Rectangle {
62 primary: Qt.LandscapeOrientation
68 primary: Qt.LandscapeOrientation
63 }
69 }
64 focus: true
70 focus: true
71+ applicationManager: ApplicationManager
72+ topLevelSurfaceList: topSurfaceList
65 }
73 }
66 }
74 }
67 }
75 }
@@ -76,6 +84,9 @@ Rectangle {
76 right: parent.right
84 right: parent.right
77 }
85 }
78
86
87+ // don't let mouse clicks in the controls area disturb the TabletStage behind it
88+ MouseArea { anchors.fill: parent }
89+
79 Column {
90 Column {
80 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
91 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
81 spacing: units.gu(1)
92 spacing: units.gu(1)
@@ -130,18 +141,21 @@ Rectangle {
130 tabletStageLoader.active = true;
141 tabletStageLoader.active = true;
131 tryCompare(tabletStageLoader, "status", Loader.Ready);
142 tryCompare(tabletStageLoader, "status", Loader.Ready);
132
143
144+ tryCompare(topSurfaceList, "count", 1);
145+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
146+
133 // this is very strange, but sometimes the test starts without
147 // this is very strange, but sometimes the test starts without
134 // TabletStage components having finished loading themselves
148 // TabletStage components having finished loading themselves
135− var appWindow = null;
149+ var appWindow = null
136 while (!appWindow) {
150 while (!appWindow) {
137− appWindow = findChild(tabletStage, "appWindow_unity8-dash");
151+ appWindow = findAppWindowForSurfaceId(topSurfaceList.idAt(0));
138 if (!appWindow) {
152 if (!appWindow) {
139− console.log("didn't find appWindow_unity8-dash in " + tabletStage + ". Trying again...");
153+ console.log("didn't find unity8-dash appWindow in " + tabletStage + ". Trying again...");
140 wait(50);
154 wait(50);
141 }
155 }
142 }
156 }
143
157
144− waitUntilAppSurfaceShowsUp("unity8-dash");
158+ waitUntilAppSurfaceShowsUp(topSurfaceList.idAt(0));
145 sideStage.hideNow()
159 sideStage.hideNow()
146 }
160 }
147
161
@@ -157,42 +171,39 @@ Rectangle {
157 // Shell instance gets destroyed.
171 // Shell instance gets destroyed.
158 tryCompare(tabletStageLoader, "itemDestroyed", true);
172 tryCompare(tabletStageLoader, "itemDestroyed", true);
159
173
160− // kill all (fake) running apps
174+ killApps();
161− webbrowserCheckBox.checked = false;
162− galleryCheckBox.checked = false;
163− dialerCheckBox.checked = false;
164− facebookCheckBox.checked = false;
165 WindowStateStorage.clear();
175 WindowStateStorage.clear();
166 }
176 }
167
177
168− function waitUntilAppSurfaceShowsUp(appId) {
178+ function waitUntilAppSurfaceShowsUp(surfaceId) {
169− var appWindow = findChild(tabletStage, "appWindow_" + appId);
179+ var appWindow = findAppWindowForSurfaceId(surfaceId);
170 verify(appWindow);
180 verify(appWindow);
171 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
181 var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
172 verify(appWindowStates);
182 verify(appWindowStates);
173 tryCompare(appWindowStates, "state", "surface");
183 tryCompare(appWindowStates, "state", "surface");
174 }
184 }
175
185
176− function switchToApp(targetAppId) {
186+ function findAppWindowForSurfaceId(surfaceId) {
177− touchFlick(tabletStage,
187+ var spreadDelegate = findChild(tabletStage, "spreadDelegate_" + surfaceId);
178− tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
188+ if (!spreadDelegate) {
179− tabletStage.x + 1, tabletStage.height / 2);
189+ return null;
180−
190+ }
181− var spreadView = findChild(tabletStage, "spreadView");
191+ var appWindow = findChild(spreadDelegate, "appWindow");
182− verify(spreadView);
192+ return appWindow;
183− tryCompare(spreadView, "phase", 2);
193+ }
184− tryCompare(spreadView, "flicking", false);
185− tryCompare(spreadView, "moving", false);
186
194
187− waitUntilAppDelegateStopsMoving(targetAppId);
195+ function switchToSurface(targetSurfaceId) {
196+ performEdgeSwipeToShowAppSpread();
188
197
189− var targetAppWindow = findChild(tabletStage, "appWindow_" + targetAppId);
198+ waitUntilAppDelegateStopsMoving(targetSurfaceId);
199+ var targetAppWindow = findAppWindowForSurfaceId(targetSurfaceId);
200+ verify(targetAppWindow);
190 tap(targetAppWindow, 10, 10);
201 tap(targetAppWindow, 10, 10);
191 }
202 }
192
203
193− function waitUntilAppDelegateStopsMoving(targetAppId)
204+ function waitUntilAppDelegateStopsMoving(targetSurfaceId)
194 {
205 {
195− var targetAppDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + targetAppId);
206+ var targetAppDelegate = findChild(tabletStage, "spreadDelegate_" + targetSurfaceId);
196 verify(targetAppDelegate);
207 verify(targetAppDelegate);
197 var lastValue = undefined;
208 var lastValue = undefined;
198 do {
209 do {
@@ -201,56 +212,86 @@ Rectangle {
201 } while (lastValue != targetAppDelegate.animatedProgress);
212 } while (lastValue != targetAppDelegate.animatedProgress);
202 }
213 }
203
214
215+ function performEdgeSwipeToShowAppSpread() {
216+ touchFlick(tabletStage,
217+ tabletStage.width - (tabletStage.dragAreaWidth / 2), tabletStage.height / 2,
218+ tabletStage.x + 1, tabletStage.height / 2);
219+
220+ var spreadView = findChild(tabletStage, "spreadView");
221+ verify(spreadView);
222+ tryCompare(spreadView, "phase", 2);
223+ tryCompare(spreadView, "flicking", false);
224+ tryCompare(spreadView, "moving", false);
225+ }
226+
227+ function swipeSurfaceUpwards(surfaceId) {
228+ var appWindow = findAppWindowForSurfaceId(surfaceId);
229+ verify(appWindow);
230+
231+ // Swipe from the left side of the surface as it's the one most likely
232+ // to not be covered by other surfaces when they're all being shown in the spread
233+ touchFlick(appWindow,
234+ appWindow.width * 0.1, appWindow.height / 2,
235+ appWindow.width * 0.1, -appWindow.height / 2);
236+ }
237+
204 function test_tappingSwitchesFocusBetweenStages() {
238 function test_tappingSwitchesFocusBetweenStages() {
205 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
239 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
206
240
241+ var webbrowserSurfaceId = topSurfaceList.nextId;
207 webbrowserCheckBox.checked = true;
242 webbrowserCheckBox.checked = true;
208− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
243+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
209 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
244 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
210 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
245 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
246+ var webbrowserWindow = findAppWindowForSurfaceId(webbrowserSurfaceId);
247+ verify(webbrowserWindow);
248+
211− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", true);
249+ tryCompare(webbrowserWindow.surface, "activeFocus", true);
212
250
251+ var dialerSurfaceId = topSurfaceList.nextId;
213 dialerCheckBox.checked = true;
252 dialerCheckBox.checked = true;
214− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
253+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
254+
215 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
255 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
256+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
257+ verify(dialerDelegate);
216− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
258+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
259+
217− tryCompare(dialerApp.session.lastSurface, "activeFocus", true);
260+ tryCompare(dialerDelegate.surface, "activeFocus", true);
218− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", false);
261+ tryCompare(webbrowserWindow.surface, "activeFocus", false);
219
262
220 // Tap on the main stage application and check if the focus
263 // Tap on the main stage application and check if the focus
221 // has been passed to it.
264 // has been passed to it.
222
265
223− var webbrowserWindow = findChild(tabletStage, "appWindow_" + webbrowserApp.appId);
224− verify(webbrowserWindow);
225 tap(webbrowserWindow);
266 tap(webbrowserWindow);
226
267
227− tryCompare(dialerApp.session.lastSurface, "activeFocus", false);
268+ tryCompare(dialerDelegate.surface, "activeFocus", false);
228− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", true);
269+ tryCompare(webbrowserWindow.surface, "activeFocus", true);
229
270
230 // Now tap on the side stage application and check if the focus
271 // Now tap on the side stage application and check if the focus
231 // has been passed back to it.
272 // has been passed back to it.
232
273
233− var dialerWindow = findChild(tabletStage, "appWindow_" + dialerApp.appId);
274+ tap(dialerDelegate);
234− verify(dialerWindow);
235− tap(dialerWindow);
236
275
237− tryCompare(dialerApp.session.lastSurface, "activeFocus", true);
276+ tryCompare(dialerDelegate.surface, "activeFocus", true);
238− tryCompare(webbrowserApp.session.lastSurface, "activeFocus", false);
277+ tryCompare(webbrowserWindow.surface, "activeFocus", false);
239 }
278 }
240
279
241 function test_closeAppInSideStage() {
280 function test_closeAppInSideStage() {
242 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
281 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
243
282
283+ var dialerSurfaceId = topSurfaceList.nextId;
244 dialerCheckBox.checked = true;
284 dialerCheckBox.checked = true;
245− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId);
285+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
246
286
247 performEdgeSwipeToShowAppSpread();
287 performEdgeSwipeToShowAppSpread();
248
288
249− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + dialerCheckBox.appId);
289+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
250 verify(appDelegate);
290 verify(appDelegate);
291+ compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
251 tryCompare(appDelegate, "swipeToCloseEnabled", true);
292 tryCompare(appDelegate, "swipeToCloseEnabled", true);
252
293
253− swipeAppUpwards(dialerCheckBox.appId);
294+ swipeSurfaceUpwards(dialerSurfaceId);
254
295
255 // Check that dialer-app has been closed
296 // Check that dialer-app has been closed
256
297
@@ -263,54 +304,37 @@ Rectangle {
263 }, null);
304 }, null);
264 }
305 }
265
306
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−
288 function test_suspendsAndResumesAppsInMainStage() {
307 function test_suspendsAndResumesAppsInMainStage() {
308+ var webbrowserSurfaceId = topSurfaceList.nextId;
289 webbrowserCheckBox.checked = true;
309 webbrowserCheckBox.checked = true;
310+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
290 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
311 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
312+ var webbrowserDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
291− compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
313+ compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
292
314
293 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
315 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
294
316
317+ var gallerySurfaceId = topSurfaceList.nextId;
295 galleryCheckBox.checked = true;
318 galleryCheckBox.checked = true;
319+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
296 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
320 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
321+ var galleryDelegate = findChild(tabletStage, "spreadDelegate_" + gallerySurfaceId);
297− compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
322+ compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
298
323
299 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
324 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
300 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
325 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
301
326
302− switchToApp(webbrowserApp.appId);
327+ switchToSurface(webbrowserSurfaceId);
303
328
304 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
329 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
305 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
330 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
306
331
307− switchToApp(galleryApp.appId);
332+ switchToSurface(gallerySurfaceId);
308
333
309 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
334 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
310 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
335 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
311 }
336 }
312
337
313−
314 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
338 function test_foregroundMainAndSideStageAppsAreKeptRunning() {
315 WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
339 WindowStateStorage.saveStage(facebookCheckBox.appId, ApplicationInfoInterface.SideStage)
316 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
340 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
@@ -321,30 +345,37 @@ Rectangle {
321 // launch two main stage apps
345 // launch two main stage apps
322 // gallery will be on foreground and webbrowser on background
346 // gallery will be on foreground and webbrowser on background
323
347
348+ var webbrowserSurfaceId = topSurfaceList.nextId;
324 webbrowserCheckBox.checked = true;
349 webbrowserCheckBox.checked = true;
325− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
350+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
326 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
351 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
327 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
352 compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
328
353
354+ var gallerySurfaceId = topSurfaceList.nextId;
329 galleryCheckBox.checked = true;
355 galleryCheckBox.checked = true;
330− waitUntilAppSurfaceShowsUp(galleryCheckBox.appId)
356+ waitUntilAppSurfaceShowsUp(gallerySurfaceId);
331 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
357 var galleryApp = ApplicationManager.findApplication(galleryCheckBox.appId);
358+ var galleryDelegate = findChild(tabletStage, "spreadDelegate_" + gallerySurfaceId);
332− compare(galleryApp.stage, ApplicationInfoInterface.MainStage);
359+ compare(galleryDelegate.stage, ApplicationInfoInterface.MainStage);
333
360
334 compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
361 compare(stagesPriv.mainStageAppId, galleryCheckBox.appId);
335
362
336 // then launch two side stage apps
363 // then launch two side stage apps
337 // facebook will be on foreground and dialer on background
364 // facebook will be on foreground and dialer on background
338
365
366+ var dialerSurfaceId = topSurfaceList.nextId;
339 dialerCheckBox.checked = true;
367 dialerCheckBox.checked = true;
340− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
368+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
341 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
369 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
370+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
342− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
371+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
343
372
373+ var facebookSurfaceId = topSurfaceList.nextId;
344 facebookCheckBox.checked = true;
374 facebookCheckBox.checked = true;
345− waitUntilAppSurfaceShowsUp(facebookCheckBox.appId)
375+ waitUntilAppSurfaceShowsUp(facebookSurfaceId);
346 var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
376 var facebookApp = ApplicationManager.findApplication(facebookCheckBox.appId);
377+ var facebookDelegate = findChild(tabletStage, "spreadDelegate_" + facebookSurfaceId);
347− compare(facebookApp.stage, ApplicationInfoInterface.SideStage);
378+ compare(facebookDelegate.stage, ApplicationInfoInterface.SideStage);
348
379
349 compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
380 compare(stagesPriv.sideStageAppId, facebookCheckBox.appId);
350
381
@@ -356,14 +387,14 @@ Rectangle {
356 tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
387 tryCompare(facebookApp, "state", ApplicationInfoInterface.Running);
357 tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
388 tryCompare(dialerApp, "state", ApplicationInfoInterface.Suspended);
358
389
359− switchToApp(dialerCheckBox.appId);
390+ switchToSurface(dialerSurfaceId);
360
391
361 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
392 tryCompare(galleryApp, "state", ApplicationInfoInterface.Running);
362 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
393 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
363 tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
394 tryCompare(facebookApp, "state", ApplicationInfoInterface.Suspended);
364 tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
395 tryCompare(dialerApp, "state", ApplicationInfoInterface.Running);
365
396
366− switchToApp(webbrowserCheckBox.appId);
397+ switchToSurface(webbrowserSurfaceId);
367
398
368 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
399 tryCompare(galleryApp, "state", ApplicationInfoInterface.Suspended);
369 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
400 tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
@@ -374,15 +405,19 @@ Rectangle {
374 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
405 function test_foregroundAppsAreSuspendedWhenStageIsSuspended() {
375 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
406 WindowStateStorage.saveStage(dialerCheckBox.appId, ApplicationInfoInterface.SideStage)
376
407
408+ var webbrowserSurfaceId = topSurfaceList.nextId;
377 webbrowserCheckBox.checked = true;
409 webbrowserCheckBox.checked = true;
378− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
410+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
379 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
411 var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
412+ var webbrowserDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
380− compare(webbrowserApp.stage, ApplicationInfoInterface.MainStage);
413+ compare(webbrowserDelegate.stage, ApplicationInfoInterface.MainStage);
381
414
415+ var dialerSurfaceId = topSurfaceList.nextId;
382 dialerCheckBox.checked = true;
416 dialerCheckBox.checked = true;
383− waitUntilAppSurfaceShowsUp(dialerCheckBox.appId)
417+ waitUntilAppSurfaceShowsUp(dialerSurfaceId);
384 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
418 var dialerApp = ApplicationManager.findApplication(dialerCheckBox.appId);
419+ var dialerDelegate = findChild(tabletStage, "spreadDelegate_" + dialerSurfaceId);
385− compare(dialerApp.stage, ApplicationInfoInterface.SideStage);
420+ compare(dialerDelegate.stage, ApplicationInfoInterface.SideStage);
386
421
387
422
388 compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
423 compare(webbrowserApp.requestedState, ApplicationInfoInterface.RequestedRunning);
@@ -467,8 +502,9 @@ Rectangle {
467 tryCompare(stagesPriv, "mainStageAppId", "unity8-dash");
502 tryCompare(stagesPriv, "mainStageAppId", "unity8-dash");
468 tryCompare(stagesPriv, "sideStageAppId", "");
503 tryCompare(stagesPriv, "sideStageAppId", "");
469
504
505+ var webbrowserSurfaceId = topSurfaceList.nextId;
470 webbrowserCheckBox.checked = true;
506 webbrowserCheckBox.checked = true;
471− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId)
507+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
472
508
473 tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
509 tryCompare(stagesPriv, "mainStageAppId", data.mainStageAppId);
474 tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
510 tryCompare(stagesPriv, "sideStageAppId", data.sideStageAppId);
@@ -476,10 +512,11 @@ Rectangle {
476
512
477 function test_loadSideStageByDragginFromMainStage() {
513 function test_loadSideStageByDragginFromMainStage() {
478 sideStage.showNow();
514 sideStage.showNow();
515+ var webbrowserSurfaceId = topSurfaceList.nextId;
479 webbrowserCheckBox.checked = true;
516 webbrowserCheckBox.checked = true;
480− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
517+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
481
518
482− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
519+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
483 verify(appDelegate);
520 verify(appDelegate);
484 compare(appDelegate.stage, ApplicationInfoInterface.MainStage);
521 compare(appDelegate.stage, ApplicationInfoInterface.MainStage);
485
522
@@ -504,10 +541,11 @@ Rectangle {
504 function test_unloadSideStageByDragginFromStageStage() {
541 function test_unloadSideStageByDragginFromStageStage() {
505 sideStage.showNow();
542 sideStage.showNow();
506 WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
543 WindowStateStorage.saveStage(webbrowserCheckBox.appId, ApplicationInfoInterface.SideStage)
544+ var webbrowserSurfaceId = topSurfaceList.nextId;
507 webbrowserCheckBox.checked = true;
545 webbrowserCheckBox.checked = true;
508− waitUntilAppSurfaceShowsUp(webbrowserCheckBox.appId);
546+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
509
547
510− var appDelegate = findChild(tabletStage, "tabletSpreadDelegate_" + webbrowserCheckBox.appId);
548+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
511 verify(appDelegate);
549 verify(appDelegate);
512 compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
550 compare(appDelegate.stage, ApplicationInfoInterface.SideStage);
513
551
@@ -527,5 +565,130 @@ Rectangle {
527
565
528 tryCompare(appDelegate, "stage", ApplicationInfoInterface.MainStage);
566 tryCompare(appDelegate, "stage", ApplicationInfoInterface.MainStage);
529 }
567 }
568+
569+ function test_closeSurfaceOfMultiSurfaceApp() {
570+ var surface1Id = topSurfaceList.nextId;
571+ webbrowserCheckBox.checked = true;
572+ waitUntilAppSurfaceShowsUp(surface1Id);
573+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
574+
575+ var surface2Id = topSurfaceList.nextId;
576+ verify(surface1Id !== surface2Id); // sanity checking
577+ webbrowserCheckBox.createSurface();
578+ waitUntilAppSurfaceShowsUp(surface2Id);
579+
580+ performEdgeSwipeToShowAppSpread();
581+
582+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + surface1Id);
583+ verify(appDelegate);
584+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
585+
586+ compare(webbrowserApp.surfaceList.count, 2);
587+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
588+
589+ swipeSurfaceUpwards(surface1Id);
590+
591+ // Surface must eventually be gone
592+ tryCompareFunction(function() { return topSurfaceList.indexForId(surface1Id); }, -1);
593+ tryCompare(webbrowserApp.surfaceList, "count", 1);
594+ compare(webbrowserApp.state, ApplicationInfoInterface.Running);
595+ }
596+
597+ /*
598+ 1- Suspended app gets killed behind the scenes, causing its surface to go zombie.
599+ 2- Surface gets screenshotted and removed. Its slot in the topSurfaceList remains,
600+ though (so ApplicationWindow can display the screenshot in its place).
601+ 3- User taps on the screenshot of the long-gone surface.
602+
603+ Expected outcome:
604+ Application gets relaunched. Its new surface will seamlessly replace the screenshot.
605+ */
606+ function test_selectSuspendedAppWithoutSurface() {
607+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
608+ var dashSurfaceId = topSurfaceList.idAt(0);
609+ var dashSurface = topSurfaceList.surfaceAt(0);
610+
611+ var webbrowserSurfaceId = topSurfaceList.nextId;
612+ webbrowserCheckBox.checked = true;
613+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
614+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
615+
616+ switchToSurface(dashSurfaceId);
617+
618+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
619+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
620+
621+ compare(webbrowserApp.surfaceList.count, 1);
622+
623+ // simulate the suspended app being killed by the out-of-memory daemon
624+ webbrowserApp.surfaceList.get(0).setLive(false);
625+
626+ // wait until the surface is gone
627+ tryCompare(webbrowserApp.surfaceList, "count", 0);
628+ compare(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
629+
630+ switchToSurface(webbrowserSurfaceId);
631+
632+ // webbrowser should have been brought to front
633+ tryCompareFunction(function(){return topSurfaceList.idAt(0);}, webbrowserSurfaceId);
634+
635+ // and it should eventually get a new surface and get resumed
636+ tryCompareFunction(function(){return topSurfaceList.surfaceAt(0) !== null;}, true);
637+ compare(topSurfaceList.count, 2); // still two top-level items
638+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Running);
639+ compare(webbrowserApp.surfaceList.count, 1);
640+ }
641+
642+ /*
643+ 1 - user suspends an application (ie, focus a surface from a different application)
644+ 2 - That suspended application gets killed, causing its surface to go zombie
645+ 3 - user goes to spread and closes that zombie surface. Actually, by that time
646+ Tablet shell will be displaying a screenshot of it instead, so there's no
647+ MirSurface whatsoever backing it up.
648+ */
649+ function test_closeZombieSurface()
650+ {
651+ compare(topSurfaceList.applicationAt(0).appId, "unity8-dash");
652+ var dashSurfaceId = topSurfaceList.idAt(0);
653+ var dashSurface = topSurfaceList.surfaceAt(0);
654+
655+ var webbrowserSurfaceId = topSurfaceList.nextId;
656+ webbrowserCheckBox.checked = true;
657+ waitUntilAppSurfaceShowsUp(webbrowserSurfaceId);
658+ var webbrowserApp = ApplicationManager.findApplication(webbrowserCheckBox.appId);
659+
660+ switchToSurface(dashSurfaceId);
661+
662+ tryCompare(MirFocusController, "focusedSurface", dashSurface);
663+ tryCompare(webbrowserApp, "state", ApplicationInfoInterface.Suspended);
664+
665+ compare(webbrowserApp.surfaceList.count, 1);
666+
667+ // simulate the suspended app being killed by the out-of-memory daemon
668+ webbrowserApp.surfaceList.get(0).setLive(false);
669+
670+ // wait until the surface is gone
671+ tryCompare(webbrowserApp.surfaceList, "count", 0);
672+ compare(topSurfaceList.surfaceAt(topSurfaceList.indexForId(webbrowserSurfaceId)), null);
673+
674+ performEdgeSwipeToShowAppSpread();
675+
676+ {
677+ var appDelegate = findChild(tabletStage, "spreadDelegate_" + webbrowserSurfaceId);
678+ verify(appDelegate);
679+ tryCompare(appDelegate, "swipeToCloseEnabled", true);
680+ }
681+
682+ swipeSurfaceUpwards(webbrowserSurfaceId);
683+
684+ // webbrowser entry is nowhere to be seen
685+ tryCompareFunction(function(){return topSurfaceList.indexForId(webbrowserSurfaceId);}, -1);
686+
687+ // nor is its app
688+ compare(ApplicationManager.findApplication(webbrowserCheckBox.appId), null);
689+
690+ // only unity8-dash surface is left
691+ compare(topSurfaceList.count, 1);
692+ }
530 }
693 }
531}
694}
tests/qmltests/Tutorial/tst_Tutorial.qml+4−8
@@ -113,6 +113,8 @@ Rectangle {
113 sourceComponent: Component {
113 sourceComponent: Component {
114 Shell {
114 Shell {
115 usageScenario: shellLoader.state
115 usageScenario: shellLoader.state
116+ nativeWidth: width
117+ nativeHeight: height
116 property string indicatorProfile: "phone"
118 property string indicatorProfile: "phone"
117 orientation: shellLoader.shellOrientation
119 orientation: shellLoader.shellOrientation
118 orientations: Orientations {
120 orientations: Orientations {
@@ -280,14 +282,6 @@ Rectangle {
280 prepareShell();
282 prepareShell();
281 }
283 }
282
284
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−
291 function swipeAwayGreeter() {
285 function swipeAwayGreeter() {
292 var coverPage = findChild(shell, "coverPage");
286 var coverPage = findChild(shell, "coverPage");
293 tryCompare(coverPage, "showProgress", 1);
287 tryCompare(coverPage, "showProgress", 1);
@@ -409,6 +403,8 @@ Rectangle {
409
403
410 var launcher = findChild(shell, "launcher");
404 var launcher = findChild(shell, "launcher");
411 var tutorialLeft = findChild(shell, "tutorialLeftLoader");
405 var tutorialLeft = findChild(shell, "tutorialLeftLoader");
406+ verify(tutorialLeft);
407+
412 touchFlick(shell, 0, halfHeight, shell.width, halfHeight);
408 touchFlick(shell, 0, halfHeight, shell.width, halfHeight);
413
409
414 tryCompare(tutorialLeft, "shown", false);
410 tryCompare(tutorialLeft, "shown", false);
tests/qmltests/tst_OrientedShell.qml+92−65
@@ -25,6 +25,7 @@ import Unity.Test 0.1
25import IntegratedLightDM 0.1 as LightDM
25import IntegratedLightDM 0.1 as LightDM
26import Powerd 0.1
26import Powerd 0.1
27import Unity.InputInfo 0.1
27import Unity.InputInfo 0.1
28+import Utils 0.1
28
29
29import "../../qml"
30import "../../qml"
30
31
@@ -432,6 +433,7 @@ Rectangle {
432
433
433 property Item orientedShell: orientedShellLoader.status === Loader.Ready ? orientedShellLoader.item : null
434 property Item orientedShell: orientedShellLoader.status === Loader.Ready ? orientedShellLoader.item : null
434 property Item shell
435 property Item shell
436+ property QtObject topLevelSurfaceList
435
437
436 SignalSpy { id: signalSpy }
438 SignalSpy { id: signalSpy }
437 SignalSpy { id: signalSpy2 }
439 SignalSpy { id: signalSpy2 }
@@ -490,6 +492,7 @@ Rectangle {
490 function cleanup() {
492 function cleanup() {
491 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
493 tryCompare(shell, "enabled", true); // make sure greeter didn't leave us in disabled state
492 shell = null;
494 shell = null;
495+ topLevelSurfaceList = null;
493
496
494 tearDown();
497 tearDown();
495 }
498 }
@@ -497,17 +500,23 @@ Rectangle {
497 function test_appSupportingOnlyPrimaryOrientationMakesPhoneShellStayPut() {
500 function test_appSupportingOnlyPrimaryOrientationMakesPhoneShellStayPut() {
498 loadShell("mako");
501 loadShell("mako");
499
502
503+ var spreadRepeater = findChild(shell, "spreadRepeater");
504+ verify(spreadRepeater);
505+
500 // unity8-dash supports only primary orientation and should be already running
506 // unity8-dash supports only primary orientation and should be already running
501− var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
507+ compare(spreadRepeater.count, 1);
502− verify(dashAppWindow);
508+ var dashDelegate = spreadRepeater.itemAt(0);
503− compare(ApplicationManager.focusedApplicationId, "unity8-dash");
504− var dashApp = dashAppWindow.application
509+ var dashApp = dashDelegate.application;
505 verify(dashApp);
510 verify(dashApp);
511+ compare(dashApp.appId, "unity8-dash");
512+ var dashAppWindow = findChild(dashDelegate, "appWindow");
513+ verify(dashAppWindow);
514+ compare(dashDelegate.focus, true);
506 compare(dashApp.rotatesWindowContents, false);
515 compare(dashApp.rotatesWindowContents, false);
507 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
516 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
508 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
517 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
509
518
510− tryCompareFunction(function(){return dashApp.session.lastSurface != null;}, true);
519+ tryCompareFunction(function(){return dashDelegate.surface != null;}, true);
511 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
520 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
512
521
513 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
522 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
@@ -538,17 +547,20 @@ Rectangle {
538 function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape(data) {
547 function test_appSupportingOnlyPrimaryOrientationWillOnlyRotateInLandscape(data) {
539 loadShell(data.deviceName);
548 loadShell(data.deviceName);
540
549
550+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
551+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
552+ var dashAppWindow = findAppWindowForSurfaceId(dashSurfaceId);
553+
541 // unity8-dash supports only primary orientation and should be already running
554 // unity8-dash supports only primary orientation and should be already running
542− var dashAppWindow = findChild(shell, "appWindow_unity8-dash");
555+
543− verify(dashAppWindow);
544 compare(ApplicationManager.focusedApplicationId, "unity8-dash");
556 compare(ApplicationManager.focusedApplicationId, "unity8-dash");
545− var dashApp = dashAppWindow.application
557+ var dashApp = dashAppWindow.application;
546 verify(dashApp);
558 verify(dashApp);
547 compare(dashApp.rotatesWindowContents, false);
559 compare(dashApp.rotatesWindowContents, false);
548 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
560 compare(dashApp.supportedOrientations, Qt.PrimaryOrientation);
549 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
561 compare(dashApp.stage, ApplicationInfoInterface.MainStage);
550
562
551− tryCompareFunction(function(){return dashApp.session.lastSurface != null;}, true);
563+ tryCompareFunction(function(){return dashApp.surfaceList.count > 0;}, true);
552 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
564 verify(checkAppSurfaceOrientation(dashAppWindow, dashApp, root.primaryOrientationAngle));
553
565
554 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
566 compare(shell.transformRotationAngle, root.primaryOrientationAngle);
@@ -615,6 +627,7 @@ Rectangle {
615 }
627 }
616 function test_appRotatesWindowContents(data) {
628 function test_appRotatesWindowContents(data) {
617 loadShell(data.deviceName);
629 loadShell(data.deviceName);
630+ var cameraSurfaceId = topLevelSurfaceList.nextId;
618 var cameraApp = ApplicationManager.startApplication("camera-app");
631 var cameraApp = ApplicationManager.startApplication("camera-app");
619 verify(cameraApp);
632 verify(cameraApp);
620
633
@@ -624,9 +637,9 @@ Rectangle {
624 compare(cameraApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
637 compare(cameraApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
625 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
638 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
626
639
627− waitUntilAppSurfaceShowsUp("camera-app")
640+ waitUntilAppWindowIsFullyLoaded(cameraSurfaceId);
628
641
629− var cameraSurface = cameraApp.session.lastSurface;
642+ var cameraSurface = cameraApp.surfaceList.get(0);
630 verify(cameraSurface);
643 verify(cameraSurface);
631
644
632 var focusChangedSpy = signalSpy;
645 var focusChangedSpy = signalSpy;
@@ -692,6 +705,7 @@ Rectangle {
692 }
705 }
693 function test_switchingToAppWithDifferentRotation(data) {
706 function test_switchingToAppWithDifferentRotation(data) {
694 loadShell(data.deviceName);
707 loadShell(data.deviceName);
708+ var gmailSurfaceId = topLevelSurfaceList.nextId;
695 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
709 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
696 verify(gmailApp);
710 verify(gmailApp);
697
711
@@ -701,8 +715,9 @@ Rectangle {
701 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
715 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
702 compare(gmailApp.stage, ApplicationInfoInterface.MainStage);
716 compare(gmailApp.stage, ApplicationInfoInterface.MainStage);
703
717
704− waitUntilAppSurfaceShowsUp("gmail-webapp")
718+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
705
719
720+ var musicSurfaceId = topLevelSurfaceList.nextId;
706 var musicApp = ApplicationManager.startApplication("music-app");
721 var musicApp = ApplicationManager.startApplication("music-app");
707 verify(musicApp);
722 verify(musicApp);
708
723
@@ -712,7 +727,7 @@ Rectangle {
712 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
727 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
713 compare(musicApp.stage, ApplicationInfoInterface.MainStage);
728 compare(musicApp.stage, ApplicationInfoInterface.MainStage);
714
729
715− waitUntilAppSurfaceShowsUp("music-app")
730+ waitUntilAppWindowIsFullyLoaded(musicSurfaceId);
716 tryCompare(shell, "orientationChangesEnabled", true);
731 tryCompare(shell, "orientationChangesEnabled", true);
717
732
718 rotateTo(90);
733 rotateTo(90);
@@ -747,6 +762,7 @@ Rectangle {
747 */
762 */
748 function test_rotateToUnsupportedDeviceOrientation(data) {
763 function test_rotateToUnsupportedDeviceOrientation(data) {
749 loadShell("mako");
764 loadShell("mako");
765+ var twitterSurfaceId = topLevelSurfaceList.nextId;
750 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
766 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
751 verify(twitterApp);
767 verify(twitterApp);
752
768
@@ -755,7 +771,7 @@ Rectangle {
755 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
771 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
756 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
772 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
757
773
758− waitUntilAppSurfaceShowsUp("twitter-webapp")
774+ waitUntilAppWindowIsFullyLoaded(twitterSurfaceId);
759
775
760 rotateTo(data.rotationAngle);
776 rotateTo(data.rotationAngle);
761 tryCompare(shell, "transformRotationAngle", data.rotationAngle);
777 tryCompare(shell, "transformRotationAngle", data.rotationAngle);
@@ -772,13 +788,14 @@ Rectangle {
772
788
773 function test_launchLandscapeOnlyAppFromPortrait() {
789 function test_launchLandscapeOnlyAppFromPortrait() {
774 loadShell("mako");
790 loadShell("mako");
791+ var weatherSurfaceId = topLevelSurfaceList.nextId;
775 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
792 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
776 verify(weatherApp);
793 verify(weatherApp);
777
794
778 // ensure the mock app is as we expect
795 // ensure the mock app is as we expect
779 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
796 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
780
797
781− waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
798+ waitUntilAppWindowIsFullyLoaded(weatherSurfaceId);
782
799
783 var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
800 var rotationStates = findInvisibleChild(orientedShell, "rotationStates");
784 waitUntilTransitionsEnd(rotationStates);
801 waitUntilTransitionsEnd(rotationStates);
@@ -810,6 +827,7 @@ Rectangle {
810 */
827 */
811 function test_greeterStaysAwayAfterRotation() {
828 function test_greeterStaysAwayAfterRotation() {
812 loadShell("mako");
829 loadShell("mako");
830+ var twitterSurfaceId = topLevelSurfaceList.nextId;
813 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
831 var twitterApp = ApplicationManager.startApplication("twitter-webapp");
814 verify(twitterApp);
832 verify(twitterApp);
815
833
@@ -818,8 +836,8 @@ Rectangle {
818 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
836 compare(twitterApp.supportedOrientations, Qt.PortraitOrientation | Qt.LandscapeOrientation
819 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
837 | Qt.InvertedPortraitOrientation | Qt.InvertedLandscapeOrientation);
820
838
821− waitUntilAppSurfaceShowsUp("twitter-webapp");
839+ waitUntilAppWindowIsFullyLoaded(twitterSurfaceId);
822− waitUntilAppWindowCanRotate("twitter-webapp");
840+ waitUntilAppWindowCanRotate(twitterSurfaceId);
823
841
824 // go back to unity8-dash
842 // go back to unity8-dash
825 performEdgeSwipeToSwitchToPreviousApp();
843 performEdgeSwipeToSwitchToPreviousApp();
@@ -835,7 +853,7 @@ Rectangle {
835 var greeter = findChild(shell, "greeter");
853 var greeter = findChild(shell, "greeter");
836 tryCompare(greeter, "animating", false);
854 tryCompare(greeter, "animating", false);
837
855
838− var twitterDelegate = findChild(shell, "appDelegate1");
856+ var twitterDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(1));
839 compare(twitterDelegate.application.appId, "twitter-webapp");
857 compare(twitterDelegate.application.appId, "twitter-webapp");
840 twitterDelegate.clicked();
858 twitterDelegate.clicked();
841
859
@@ -908,9 +926,9 @@ Rectangle {
908 verify(item.application.appId, "dialer-app");
926 verify(item.application.appId, "dialer-app");
909 }
927 }
910
928
929+ WindowStateStorage.saveStage("dialer-app", ApplicationInfoInterface.SideStage)
911 var dialerApp = ApplicationManager.startApplication("dialer-app");
930 var dialerApp = ApplicationManager.startApplication("dialer-app");
912 verify(dialerApp);
931 verify(dialerApp);
913− dialerApp.stage = ApplicationInfoInterface.SideStage;
914
932
915 // ensure the mock dialer-app is as we expect
933 // ensure the mock dialer-app is as we expect
916 compare(dialerApp.rotatesWindowContents, false);
934 compare(dialerApp.rotatesWindowContents, false);
@@ -955,9 +973,9 @@ Rectangle {
955 verify(item.application.appId, "dialer-app");
973 verify(item.application.appId, "dialer-app");
956 }
974 }
957
975
976+ WindowStateStorage.saveStage("dialer-app", ApplicationInfoInterface.SideStage)
958 var dialerApp = ApplicationManager.startApplication("dialer-app");
977 var dialerApp = ApplicationManager.startApplication("dialer-app");
959 verify(dialerApp);
978 verify(dialerApp);
960− dialerApp.stage = ApplicationInfoInterface.SideStage;
961
979
962 // ensure the mock dialer-app is as we expect
980 // ensure the mock dialer-app is as we expect
963 compare(dialerApp.rotatesWindowContents, false);
981 compare(dialerApp.rotatesWindowContents, false);
@@ -1023,13 +1041,15 @@ Rectangle {
1023 function test_launchedAppHasActiveFocus(data) {
1041 function test_launchedAppHasActiveFocus(data) {
1024 loadShell(data.deviceName);
1042 loadShell(data.deviceName);
1025
1043
1044+ var gmailSurfaceId = topLevelSurfaceList.nextId;
1026 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
1045 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
1027 verify(gmailApp);
1046 verify(gmailApp);
1028− waitUntilAppSurfaceShowsUp("gmail-webapp")
1047+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
1029
1048
1030− verify(gmailApp.session.lastSurface);
1049+ var gmailSurface = gmailApp.surfaceList.get(0);
1050+ verify(gmailSurface);
1031
1051
1032− tryCompare(gmailApp.session.lastSurface, "activeFocus", true);
1052+ tryCompare(gmailSurface, "activeFocus", true);
1033 }
1053 }
1034
1054
1035 function test_launchLandscapeOnlyAppOverPortraitOnlyDashThenSwitchToDash() {
1055 function test_launchLandscapeOnlyAppOverPortraitOnlyDashThenSwitchToDash() {
@@ -1038,13 +1058,14 @@ Rectangle {
1038 // starts as portrait, as unity8-dash is portrait only
1058 // starts as portrait, as unity8-dash is portrait only
1039 tryCompare(shell, "transformRotationAngle", 0);
1059 tryCompare(shell, "transformRotationAngle", 0);
1040
1060
1061+ var weatherSurfaceId = topLevelSurfaceList.nextId;
1041 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
1062 var weatherApp = ApplicationManager.startApplication("ubuntu-weather-app");
1042 verify(weatherApp);
1063 verify(weatherApp);
1043
1064
1044 // ensure the mock app is as we expect
1065 // ensure the mock app is as we expect
1045 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
1066 compare(weatherApp.supportedOrientations, Qt.LandscapeOrientation | Qt.InvertedLandscapeOrientation);
1046
1067
1047− waitUntilAppSurfaceShowsUp("ubuntu-weather-app");
1068+ waitUntilAppWindowIsFullyLoaded(weatherSurfaceId);
1048
1069
1049 // should have rotated to landscape
1070 // should have rotated to landscape
1050 tryCompareFunction(function () { return shell.transformRotationAngle == 270
1071 tryCompareFunction(function () { return shell.transformRotationAngle == 270
@@ -1243,10 +1264,14 @@ Rectangle {
1243 function test_lockPhoneAfterClosingAppInSpreadThenUnlockAndRotate() {
1264 function test_lockPhoneAfterClosingAppInSpreadThenUnlockAndRotate() {
1244 loadShell("mako");
1265 loadShell("mako");
1245
1266
1267+ compare(topLevelSurfaceList.applicationAt(0).appId, "unity8-dash");
1268+ var dashSurfaceId = topLevelSurfaceList.idAt(0);
1269+
1270+ var gmailSurfaceId = topLevelSurfaceList.nextId;
1246 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
1271 var gmailApp = ApplicationManager.startApplication("gmail-webapp");
1247 verify(gmailApp);
1272 verify(gmailApp);
1248
1273
1249− waitUntilAppSurfaceShowsUp("gmail-webapp");
1274+ waitUntilAppWindowIsFullyLoaded(gmailSurfaceId);
1250
1275
1251 performEdgeSwipeToShowAppSpread();
1276 performEdgeSwipeToShowAppSpread();
1252
1277
@@ -1262,7 +1287,7 @@ Rectangle {
1262
1287
1263 swipeAwayGreeter();
1288 swipeAwayGreeter();
1264
1289
1265− verify(isAppSurfaceFocused("unity8-dash"))
1290+ verify(isAppSurfaceFocused(dashSurfaceId))
1266
1291
1267 signalSpy.clear();
1292 signalSpy.clear();
1268 signalSpy.target = shell;
1293 signalSpy.target = shell;
@@ -1324,6 +1349,7 @@ Rectangle {
1324 ////
1349 ////
1325 // Launch a portrait-only application
1350 // Launch a portrait-only application
1326
1351
1352+ var dialerSurfaceId = topLevelSurfaceList.nextId;
1327 var dialerApp = ApplicationManager.startApplication("dialer-app");
1353 var dialerApp = ApplicationManager.startApplication("dialer-app");
1328 verify(dialerApp);
1354 verify(dialerApp);
1329
1355
@@ -1331,9 +1357,9 @@ Rectangle {
1331 compare(dialerApp.rotatesWindowContents, false);
1357 compare(dialerApp.rotatesWindowContents, false);
1332 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
1358 compare(dialerApp.supportedOrientations, Qt.PortraitOrientation | Qt.InvertedPortraitOrientation);
1333
1359
1334− waitUntilAppSurfaceShowsUp("dialer-app");
1360+ waitUntilAppWindowIsFullyLoaded(dialerSurfaceId);
1335− waitUntilAppWindowCanRotate("dialer-app");
1361+ waitUntilAppWindowCanRotate(dialerSurfaceId);
1336− verify(isAppSurfaceFocused("dialer-app"));
1362+ verify(isAppSurfaceFocused(dialerSurfaceId));
1337
1363
1338 ////
1364 ////
1339 // check outcome (shell should stay in landscape)
1365 // check outcome (shell should stay in landscape)
@@ -1386,17 +1412,29 @@ Rectangle {
1386 tryCompare(appWindowStates, "state", "surface");
1412 tryCompare(appWindowStates, "state", "surface");
1387 }
1413 }
1388
1414
1389− function waitUntilAppSurfaceShowsUp(appId) {
1415+ function findAppWindowForSurfaceId(surfaceId) {
1390− var appWindow = findChild(shell, "appWindow_" + appId);
1416+ // for PhoneStage and TabletStage
1391− verify(appWindow);
1417+ var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
1392− var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
1418+ if (!delegate) {
1393− verify(appWindowStates);
1419+ // for DesktopStage
1394− tryCompare(appWindowStates, "state", "surface");
1420+ delegate = findChild(shell, "appDelegate_" + surfaceId);
1421+ }
1422+ verify(delegate);
1423+ var appWindow = findChild(delegate, "appWindow");
1424+ return appWindow;
1395 }
1425 }
1396
1426
1427+ // Wait until the ApplicationWindow for the given Application object is fully loaded
1428+ // (ie, the real surface has replaced the splash screen)
1429+ function waitUntilAppWindowIsFullyLoaded(surfaceId) {
1430+ var appWindow = findAppWindowForSurfaceId(surfaceId);
1431+ var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
1432+ tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
1433+ waitUntilTransitionsEnd(appWindowStateGroup);
1434+ }
1435+
1397− function waitUntilAppWindowCanRotate(appId) {
1436+ function waitUntilAppWindowCanRotate(surfaceId) {
1398− var appWindow = findChild(shell, "appWindow_" + appId);
1437+ var appWindow = findAppWindowForSurfaceId(surfaceId);
1399− verify(appWindow);
1400 tryCompare(appWindow, "orientationChangesEnabled", true);
1438 tryCompare(appWindow, "orientationChangesEnabled", true);
1401 }
1439 }
1402
1440
@@ -1426,8 +1464,8 @@ Rectangle {
1426 var spreadView = findChild(shell, "spreadView");
1464 var spreadView = findChild(shell, "spreadView");
1427 verify(spreadView);
1465 verify(spreadView);
1428
1466
1429− verify(ApplicationManager.count >= 2);
1467+ verify(topLevelSurfaceList.count >= 2);
1430− var previousApp = ApplicationManager.get(1);
1468+ var previousSurfaceId = topLevelSurfaceList.idAt(1);
1431
1469
1432 var touchStartX = shell.width - 1;
1470 var touchStartX = shell.width - 1;
1433 var touchStartY = shell.height / 2;
1471 var touchStartY = shell.height / 2;
@@ -1453,7 +1491,7 @@ Rectangle {
1453 // ensure the app switch animation has ended
1491 // ensure the app switch animation has ended
1454 tryCompare(spreadView, "shiftedContentX", 0);
1492 tryCompare(spreadView, "shiftedContentX", 0);
1455
1493
1456− compare(ApplicationManager.get(0).appId, previousApp.appId);
1494+ tryCompareFunction(function(){ return topLevelSurfaceList.idAt(0); }, previousSurfaceId);
1457 }
1495 }
1458
1496
1459 function performEdgeSwipeToShowAppSpread() {
1497 function performEdgeSwipeToShowAppSpread() {
@@ -1508,14 +1546,6 @@ Rectangle {
1508 tryCompare(greeter, "fullyShown", true);
1546 tryCompare(greeter, "fullyShown", true);
1509 }
1547 }
1510
1548
1511− function killApps() {
1512− while (ApplicationManager.count > 1) {
1513− var appIndex = ApplicationManager.get(0).appId == "unity8-dash" ? 1 : 0
1514− ApplicationManager.stopApplication(ApplicationManager.get(appIndex).appId);
1515− }
1516− compare(ApplicationManager.count, 1)
1517− }
1518−
1519 function loadShell(deviceName) {
1549 function loadShell(deviceName) {
1520 applicationArguments.deviceName = deviceName;
1550 applicationArguments.deviceName = deviceName;
1521
1551
@@ -1527,6 +1557,12 @@ Rectangle {
1527
1557
1528 shell = findChild(orientedShell, "shell");
1558 shell = findChild(orientedShell, "shell");
1529
1559
1560+ topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
1561+ verify(topLevelSurfaceList);
1562+
1563+ var stageLoader = findChild(shell, "applicationsDisplayLoader");
1564+ verify(stageLoader);
1565+
1530 tryCompare(shell, "enabled", true); // enabled by greeter when ready
1566 tryCompare(shell, "enabled", true); // enabled by greeter when ready
1531
1567
1532 waitUntilShellIsInOrientation(root.physicalOrientation0);
1568 waitUntilShellIsInOrientation(root.physicalOrientation0);
@@ -1535,7 +1571,7 @@ Rectangle {
1535
1571
1536 swipeAwayGreeter();
1572 swipeAwayGreeter();
1537
1573
1538− var spreadRepeater = findChild(shell, "spreadRepeater");
1574+ var spreadRepeater = findChild(stageLoader, "spreadRepeater");
1539 if (spreadRepeater) {
1575 if (spreadRepeater) {
1540 spreadRepeaterConnections.target = spreadRepeater;
1576 spreadRepeaterConnections.target = spreadRepeater;
1541 }
1577 }
@@ -1556,7 +1592,7 @@ Rectangle {
1556
1592
1557 // expectedAngle is in orientedShell's coordinate system
1593 // expectedAngle is in orientedShell's coordinate system
1558 function checkAppSurfaceOrientation(item, app, expectedAngle) {
1594 function checkAppSurfaceOrientation(item, app, expectedAngle) {
1559− var surface = app.session.lastSurface;
1595+ var surface = app.surfaceList.get(0);
1560 if (!surface) {
1596 if (!surface) {
1561 console.warn("no surface");
1597 console.warn("no surface");
1562 return false;
1598 return false;
@@ -1611,7 +1647,7 @@ Rectangle {
1611 var spreadView = findChild(shell, "spreadView");
1647 var spreadView = findChild(shell, "spreadView");
1612 verify(spreadView);
1648 verify(spreadView);
1613
1649
1614− var delegateToClose = findChild(spreadView, "appDelegate0");
1650+ var delegateToClose = findChild(spreadView, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
1615 verify(delegateToClose);
1651 verify(delegateToClose);
1616
1652
1617 var appIdToClose = ApplicationManager.get(0).appId;;
1653 var appIdToClose = ApplicationManager.get(0).appId;;
@@ -1637,20 +1673,11 @@ Rectangle {
1637 compare(ApplicationManager.findApplication(appIdToClose), null);
1673 compare(ApplicationManager.findApplication(appIdToClose), null);
1638 }
1674 }
1639
1675
1640− function isAppSurfaceFocused(appId) {
1676+ function isAppSurfaceFocused(surfaceId) {
1641− var appWindow = findChild(shell, "appWindow_" + appId);
1677+ var index = topLevelSurfaceList.indexForId(surfaceId);
1642− verify(appWindow);
1678+ var surface = topLevelSurfaceList.surfaceAt(index);
1643−
1644− var app = ApplicationManager.findApplication(appId);
1645− verify(app);
1646−
1647− var surface = app.session.lastSurface;
1648 verify(surface);
1679 verify(surface);
1649−
1650− var surfaceItem = findSurfaceItem(appWindow, surface);
1651− verify(surfaceItem);
1652−
1653− return surfaceItem.activeFocus;
1680+ return surface.activeFocus;
1654 }
1681 }
1655 }
1682 }
1656}
1683}
tests/qmltests/tst_Shell.qml+233−160
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013-2015 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -131,9 +131,6 @@ Rectangle {
131 anchors.right: root.right
131 anchors.right: root.right
132 width: units.gu(30)
132 width: units.gu(30)
133
133
134− property var focusedApp: ApplicationManager.findApplication(ApplicationManager.focusedApplicationId)
135− property var focusedSurface: focusedApp && focusedApp.session ? focusedApp.session.lastSurface : null
136−
137 Rectangle {
134 Rectangle {
138 id: controlRect
135 id: controlRect
139 anchors { left: parent.left; right: parent.right }
136 anchors { left: parent.left; right: parent.right }
@@ -185,6 +182,17 @@ Rectangle {
185 }
182 }
186 }
183 }
187 }
184 }
185+ Button {
186+ text: "Show Launcher"
187+ activeFocusOnPress: false
188+ onClicked: {
189+ if (shellLoader.status !== Loader.Ready)
190+ return;
191+
192+ var launcher = testCase.findChild(shellLoader.item, "launcher");
193+ launcher.state = "visible";
194+ }
195+ }
188 ListItem.ItemSelector {
196 ListItem.ItemSelector {
189 anchors { left: parent.left; right: parent.right }
197 anchors { left: parent.left; right: parent.right }
190 activeFocusOnPress: false
198 activeFocusOnPress: false
@@ -274,21 +282,21 @@ Rectangle {
274 id: fullscreeAppCheck
282 id: fullscreeAppCheck
275
283
276 onTriggered: {
284 onTriggered: {
277− if (!controls.focusedSurface) return;
285+ if (!MirFocusController.focusedSurface) return;
278− if (controls.focusedSurface.state == Mir.FullscreenState) {
286+ if (MirFocusController.focusedSurface.state == Mir.FullscreenState) {
279− controls.focusedSurface.state = Mir.RestoredState;
287+ MirFocusController.focusedSurface.state = Mir.RestoredState;
280 } else {
288 } else {
281− controls.focusedSurface.state = Mir.FullscreenState;
289+ MirFocusController.focusedSurface.state = Mir.FullscreenState;
282 }
290 }
283 }
291 }
284
292
285 Binding {
293 Binding {
286 target: fullscreeAppCheck
294 target: fullscreeAppCheck
287− when: controls.focusedSurface
295+ when: MirFocusController.focusedSurface
288 property: "checked"
296 property: "checked"
289 value: {
297 value: {
290− if (!controls.focusedSurface) return false;
298+ if (!MirFocusController.focusedSurface) return false;
291− return controls.focusedSurface.state === Mir.FullscreenState
299+ return MirFocusController.focusedSurface.state === Mir.FullscreenState
292 }
300 }
293 }
301 }
294 }
302 }
@@ -302,21 +310,21 @@ Rectangle {
302 id: chromeAppCheck
310 id: chromeAppCheck
303
311
304 onTriggered: {
312 onTriggered: {
305− if (!controls.focusedSurface) return;
313+ if (!MirFocusController.focusedSurface) return;
306− if (controls.focusedSurface.shellChrome == Mir.LowChrome) {
314+ if (MirFocusController.focusedSurface.shellChrome == Mir.LowChrome) {
307− controls.focusedSurface.setShellChrome(Mir.NormalChrome);
315+ MirFocusController.focusedSurface.setShellChrome(Mir.NormalChrome);
308 } else {
316 } else {
309− controls.focusedSurface.setShellChrome(Mir.LowChrome);
317+ MirFocusController.focusedSurface.setShellChrome(Mir.LowChrome);
310 }
318 }
311 }
319 }
312
320
313 Binding {
321 Binding {
314 target: chromeAppCheck
322 target: chromeAppCheck
315− when: controls.focusedSurface !== null
323+ when: MirFocusController.focusedSurface !== null
316 property: "checked"
324 property: "checked"
317 value: {
325 value: {
318− if (!controls.focusedSurface) return false;
326+ if (!MirFocusController.focusedSurface) return false;
319− controls.focusedSurface.shellChrome === Mir.LowChrome
327+ MirFocusController.focusedSurface.shellChrome === Mir.LowChrome
320 }
328 }
321 }
329 }
322 }
330 }
@@ -381,12 +389,6 @@ Rectangle {
381 signalName: "actionInvoked"
389 signalName: "actionInvoked"
382 }
390 }
383
391
384− SignalSpy {
385− id: appRemovedSpy
386− target: ApplicationManager
387− signalName: "applicationRemoved"
388− }
389−
390 Telephony.CallEntry {
392 Telephony.CallEntry {
391 id: phoneCall
393 id: phoneCall
392 phoneNumber: "+447812221111"
394 phoneNumber: "+447812221111"
@@ -412,6 +414,7 @@ Rectangle {
412 when: windowShown
414 when: windowShown
413
415
414 property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
416 property Item shell: shellLoader.status === Loader.Ready ? shellLoader.item : null
417+ property QtObject topLevelSurfaceList: null
415
418
416 function init() {
419 function init() {
417 if (shellLoader.active) {
420 if (shellLoader.active) {
@@ -449,6 +452,9 @@ Rectangle {
449 panel.dismissTimer = fakeDismissTimer;
452 panel.dismissTimer = fakeDismissTimer;
450
453
451 waitForGreeterToStabilize();
454 waitForGreeterToStabilize();
455+
456+ topLevelSurfaceList = findInvisibleChild(shell, "topLevelSurfaceList");
457+ verify(topLevelSurfaceList);
452 }
458 }
453
459
454 function loadDesktopShellWithApps() {
460 function loadDesktopShellWithApps() {
@@ -463,7 +469,9 @@ Rectangle {
463 var app5 = ApplicationManager.startApplication("camera-app")
469 var app5 = ApplicationManager.startApplication("camera-app")
464 var app6 = ApplicationManager.startApplication("gallery-app")
470 var app6 = ApplicationManager.startApplication("gallery-app")
465 var app7 = ApplicationManager.startApplication("calendar-app")
471 var app7 = ApplicationManager.startApplication("calendar-app")
472+ for (var i = 0; i < topLevelSurfaceList.count; ++i) {
466− waitUntilAppWindowIsFullyLoaded(app7);
473+ waitUntilAppWindowIsFullyLoaded(topLevelSurfaceList.idAt(i));
474+ }
467 }
475 }
468
476
469 function waitForGreeterToStabilize() {
477 function waitForGreeterToStabilize() {
@@ -503,7 +511,7 @@ Rectangle {
503 Wizard.System.wizardEnabled = false;
511 Wizard.System.wizardEnabled = false;
504
512
505 // kill all (fake) running apps
513 // kill all (fake) running apps
506− killApps(ApplicationManager);
514+ killApps();
507
515
508 unlockAllModemsSpy.clear()
516 unlockAllModemsSpy.clear()
509 LightDM.Greeter.authenticate(""); // reset greeter
517 LightDM.Greeter.authenticate(""); // reset greeter
@@ -511,26 +519,25 @@ Rectangle {
511 sessionSpy.clear();
519 sessionSpy.clear();
512
520
513 GSettingsController.setLifecycleExemptAppids([]);
521 GSettingsController.setLifecycleExemptAppids([]);
514− }
515
522
516− function killApps() {
523+ // there should be only unity8-dash window over there
517− while (ApplicationManager.count > 1) {
524+ tryCompare(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)
525+ compare(ApplicationManager.get(0).appId, "unity8-dash");
522 }
526 }
523
527
524 function test_snapDecisionDismissalReturnsFocus() {
528 function test_snapDecisionDismissalReturnsFocus() {
525 loadShell("phone");
529 loadShell("phone");
526 swipeAwayGreeter();
530 swipeAwayGreeter();
527 var notifications = findChild(shell, "notificationList");
531 var notifications = findChild(shell, "notificationList");
532+ var appSurfaceId = topLevelSurfaceList.nextId;
528 var app = ApplicationManager.startApplication("camera-app");
533 var app = ApplicationManager.startApplication("camera-app");
534+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
529 var stage = findChild(shell, "stage")
535 var stage = findChild(shell, "stage")
530− // Open an application and focus
536+
531− waitUntilApplicationWindowIsFullyVisible(app);
537+ var appSurface = app.surfaceList.get(0);
532− ApplicationManager.focusApplication(app);
538+ verify(appSurface);
539+
533− tryCompare(app.session.lastSurface, "activeFocus", true);
540+ tryCompare(appSurface, "activeFocus", true);
534
541
535 notifications.model = mockNotificationsModel;
542 notifications.model = mockNotificationsModel;
536
543
@@ -552,7 +559,7 @@ Rectangle {
552 waitForRendering(notification);
559 waitForRendering(notification);
553
560
554 // Make sure activeFocus went away from the app window
561 // Make sure activeFocus went away from the app window
555− tryCompare(app.session.lastSurface, "activeFocus", false);
562+ tryCompare(appSurface, "activeFocus", false);
556 tryCompare(stage, "interactive", false);
563 tryCompare(stage, "interactive", false);
557
564
558 // Clicking the button should dismiss the notification and return focus
565 // Clicking the button should dismiss the notification and return focus
@@ -560,7 +567,7 @@ Rectangle {
560 mouseClick(buttonAccept);
567 mouseClick(buttonAccept);
561
568
562 // Make sure we're back to normal
569 // Make sure we're back to normal
563− tryCompare(app.session.lastSurface, "activeFocus", true);
570+ tryCompare(appSurface, "activeFocus", true);
564 compare(stage.interactive, true, "Stages not interactive again after modal notification has closed");
571 compare(stage.interactive, true, "Stages not interactive again after modal notification has closed");
565 }
572 }
566
573
@@ -618,10 +625,9 @@ Rectangle {
618 loadShell("phone");
625 loadShell("phone");
619 swipeAwayGreeter();
626 swipeAwayGreeter();
620 dragLauncherIntoView();
627 dragLauncherIntoView();
628+ var appSurfaceId = topLevelSurfaceList.nextId;
621 tapOnAppIconInLauncher();
629 tapOnAppIconInLauncher();
622− waitUntilApplicationWindowIsFullyVisible();
630+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
623− ApplicationManager.focusApplication(data.focusedApp)
624− waitUntilApplicationWindowIsFullyVisible();
625
631
626 var greeter = findChild(shell, "greeter");
632 var greeter = findChild(shell, "greeter");
627 if (data.greeterShown) {
633 if (data.greeterShown) {
@@ -637,16 +643,18 @@ Rectangle {
637 waitUntilDashIsFocused();
643 waitUntilDashIsFocused();
638 tryCompare(greeter, "shown", false);
644 tryCompare(greeter, "shown", false);
639 } else {
645 } else {
640− waitUntilApplicationWindowIsFullyVisible();
641 compare(greeter.fullyShown, data.greeterShown);
646 compare(greeter.fullyShown, data.greeterShown);
642 }
647 }
643
648
644 var launcher = findChild(shell, "launcherPanel");
649 var launcher = findChild(shell, "launcherPanel");
645 tryCompare(launcher, "x", data.launcherHides ? -launcher.width : 0)
650 tryCompare(launcher, "x", data.launcherHides ? -launcher.width : 0)
646
651
652+ //FIXME: This check fails. Don't understand the rationale behind it.
653+ /*
647 // Make sure the helper for sliding out the launcher wasn't touched. We want to fade it out here.
654 // Make sure the helper for sliding out the launcher wasn't touched. We want to fade it out here.
648 var animateTimer = findInvisibleChild(shell, "animateTimer");
655 var animateTimer = findInvisibleChild(shell, "animateTimer");
649 compare(animateTimer.nextState, "visible");
656 compare(animateTimer.nextState, "visible");
657+ */
650 }
658 }
651
659
652 function test_tabletLeftEdgeDrag_data() {
660 function test_tabletLeftEdgeDrag_data() {
@@ -673,10 +681,11 @@ Rectangle {
673 dragLauncherIntoView();
681 dragLauncherIntoView();
674 dashCommunicatorSpy.clear();
682 dashCommunicatorSpy.clear();
675
683
684+ var appSurfaceId = topLevelSurfaceList.nextId;
676 tapOnAppIconInLauncher();
685 tapOnAppIconInLauncher();
677− waitUntilApplicationWindowIsFullyVisible();
686+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
678
687
679− verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
688+ tryCompareFunction(function() { return ApplicationManager.focusedApplicationId !== "unity8-dash"; }, true);
680
689
681 //Long left swipe
690 //Long left swipe
682 swipeFromLeftEdge(units.gu(30));
691 swipeFromLeftEdge(units.gu(30));
@@ -709,8 +718,9 @@ Rectangle {
709
718
710 // Launch an app from the launcher
719 // Launch an app from the launcher
711 dragLauncherIntoView();
720 dragLauncherIntoView();
721+ var appSurfaceId = topLevelSurfaceList.nextId;
712 tapOnAppIconInLauncher();
722 tapOnAppIconInLauncher();
713− waitUntilApplicationWindowIsFullyVisible();
723+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
714
724
715 var mainAppId = ApplicationManager.focusedApplicationId;
725 var mainAppId = ApplicationManager.focusedApplicationId;
716 verify(mainAppId != "");
726 verify(mainAppId != "");
@@ -835,9 +845,9 @@ Rectangle {
835 dragLauncherIntoView();
845 dragLauncherIntoView();
836
846
837 // Launch an app from the launcher
847 // Launch an app from the launcher
848+ var appSurfaceId = topLevelSurfaceList.nextId;
838 tapOnAppIconInLauncher();
849 tapOnAppIconInLauncher();
839−
850+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
840− waitUntilApplicationWindowIsFullyVisible();
841
851
842 verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
852 verify(ApplicationManager.focusedApplicationId !== "unity8-dash")
843
853
@@ -873,10 +883,22 @@ Rectangle {
873 tryCompare(item, "visible", false);
883 tryCompare(item, "visible", false);
874 }
884 }
875
885
886+ function findAppWindowForSurfaceId(surfaceId) {
887+ // for PhoneStage and TabletStage
888+ var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
889+ if (!delegate) {
890+ // for DesktopStage
891+ delegate = findChild(shell, "appDelegate_" + surfaceId);
892+ }
893+ verify(delegate);
894+ var appWindow = findChild(delegate, "appWindow");
895+ return appWindow;
896+ }
897+
876 // Wait until the ApplicationWindow for the given Application object is fully loaded
898 // Wait until the ApplicationWindow for the given Application object is fully loaded
877 // (ie, the real surface has replaced the splash screen)
899 // (ie, the real surface has replaced the splash screen)
878− function waitUntilAppWindowIsFullyLoaded(app) {
900+ function waitUntilAppWindowIsFullyLoaded(surfaceId) {
879− var appWindow = findChild(shell, "appWindow_" + app.appId);
901+ var appWindow = findAppWindowForSurfaceId(surfaceId);
880 var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
902 var appWindowStateGroup = findInvisibleChild(appWindow, "applicationWindowStateGroup");
881 tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
903 tryCompareFunction(function() { return appWindowStateGroup.state === "surface" }, true);
882 waitUntilTransitionsEnd(appWindowStateGroup);
904 waitUntilTransitionsEnd(appWindowStateGroup);
@@ -885,10 +907,13 @@ Rectangle {
885 function test_surfaceLosesActiveFocusWhilePanelIsOpen() {
907 function test_surfaceLosesActiveFocusWhilePanelIsOpen() {
886 loadShell("phone");
908 loadShell("phone");
887 swipeAwayGreeter();
909 swipeAwayGreeter();
910+ var appSurfaceId = topLevelSurfaceList.nextId;
888 var app = ApplicationManager.startApplication("dialer-app");
911 var app = ApplicationManager.startApplication("dialer-app");
889− waitUntilAppWindowIsFullyLoaded(app);
912+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
913+ var appSurface = app.surfaceList.get(0);
914+ verify(appSurface);
890
915
891− tryCompare(app.session.lastSurface, "activeFocus", true);
916+ tryCompare(appSurface, "activeFocus", true);
892
917
893 // Drag the indicators panel half-open
918 // Drag the indicators panel half-open
894 var touchX = shell.width / 2;
919 var touchX = shell.width / 2;
@@ -899,7 +924,7 @@ Rectangle {
899 true /* beginTouch */, false /* endTouch */);
924 true /* beginTouch */, false /* endTouch */);
900 verify(indicators.partiallyOpened);
925 verify(indicators.partiallyOpened);
901
926
902− tryCompare(app.session.lastSurface, "activeFocus", false);
927+ tryCompare(appSurface, "activeFocus", false);
903
928
904 // And finish getting it open
929 // And finish getting it open
905 touchFlick(indicators,
930 touchFlick(indicators,
@@ -908,11 +933,11 @@ Rectangle {
908 false /* beginTouch */, true /* endTouch */);
933 false /* beginTouch */, true /* endTouch */);
909 tryCompare(indicators, "fullyOpened", true);
934 tryCompare(indicators, "fullyOpened", true);
910
935
911− tryCompare(app.session.lastSurface, "activeFocus", false);
936+ tryCompare(appSurface, "activeFocus", false);
912
937
913 dragToCloseIndicatorsPanel();
938 dragToCloseIndicatorsPanel();
914
939
915− tryCompare(app.session.lastSurface, "activeFocus", true);
940+ tryCompare(appSurface, "activeFocus", true);
916 }
941 }
917
942
918 function test_launchedAppHasActiveFocus_data() {
943 function test_launchedAppHasActiveFocus_data() {
@@ -928,54 +953,44 @@ Rectangle {
928 shell.usageScenario = data.usageScenario;
953 shell.usageScenario = data.usageScenario;
929 swipeAwayGreeter();
954 swipeAwayGreeter();
930
955
956+ var webAppSurfaceId = topLevelSurfaceList.nextId;
931 var webApp = ApplicationManager.startApplication("webbrowser-app");
957 var webApp = ApplicationManager.startApplication("webbrowser-app");
932 verify(webApp);
958 verify(webApp);
933− waitUntilAppSurfaceShowsUp("webbrowser-app")
959+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
934
960
935− verify(webApp.session.lastSurface);
961+ var webAppSurface = webApp.surfaceList.get(topLevelSurfaceList.indexForId(webAppSurfaceId));
962+ verify(webAppSurface);
936
963
937− tryCompare(webApp.session.lastSurface, "activeFocus", true);
964+ tryCompare(webAppSurface, "activeFocus", true);
938 }
965 }
939
966
940 function test_launchedAppKeepsActiveFocusOnUsageModeChange() {
967 function test_launchedAppKeepsActiveFocusOnUsageModeChange() {
941 loadShell("tablet");
968 loadShell("tablet");
942 swipeAwayGreeter();
969 swipeAwayGreeter();
943
970
971+ var webAppSurfaceId = topLevelSurfaceList.nextId;
944 var webApp = ApplicationManager.startApplication("webbrowser-app");
972 var webApp = ApplicationManager.startApplication("webbrowser-app");
945 verify(webApp);
973 verify(webApp);
946− waitUntilAppSurfaceShowsUp("webbrowser-app")
974+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
947
975
948− verify(webApp.session.lastSurface);
976+ var webAppSurface = webApp.surfaceList.get(topLevelSurfaceList.indexForId(webAppSurfaceId));
977+ verify(webAppSurface);
949
978
950− tryCompare(webApp.session.lastSurface, "activeFocus", true);
979+ tryCompare(webAppSurface, "activeFocus", true);
951
980
952 shell.usageScenario = "desktop";
981 shell.usageScenario = "desktop";
953
982
954 // check that the desktop stage and window have been loaded
983 // check that the desktop stage and window have been loaded
955− {
984+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
956− var desktopWindow = findChild(shell, "appWindow_webbrowser-app");
957− verify(desktopWindow);
958− }
959
985
960− tryCompare(webApp.session.lastSurface, "activeFocus", true);
986+ tryCompare(webAppSurface, "activeFocus", true);
961
987
962 shell.usageScenario = "tablet";
988 shell.usageScenario = "tablet";
963
989
964 // check that the tablet stage and app surface delegate have been loaded
990 // check that the tablet stage and app surface delegate have been loaded
965− {
991+ waitUntilAppWindowIsFullyLoaded(webAppSurfaceId);
966− var desktopWindow = findChild(shell, "tabletSpreadDelegate_webbrowser-app");
967− verify(desktopWindow);
968− }
969
992
970− tryCompare(webApp.session.lastSurface, "activeFocus", true);
993+ tryCompare(webAppSurface, "activeFocus", true);
971− }
972−
973− function waitUntilAppSurfaceShowsUp(appId) {
974− var appWindow = findChild(shell, "appWindow_" + appId);
975− verify(appWindow);
976− var appWindowStates = findInvisibleChild(appWindow, "applicationWindowStateGroup");
977− verify(appWindowStates);
978− tryCompare(appWindowStates, "state", "surface");
979 }
994 }
980
995
981 function dragToCloseIndicatorsPanel() {
996 function dragToCloseIndicatorsPanel() {
@@ -1025,12 +1040,6 @@ Rectangle {
1025 }
1040 }
1026 }
1041 }
1027
1042
1028− function waitUntilApplicationWindowIsFullyVisible() {
1029− var appDelegate = findChild(shell, "appDelegate0")
1030− var surfaceContainer = findChild(appDelegate, "surfaceContainer");
1031− tryCompareFunction(function() { return surfaceContainer.surface !== null; }, true);
1032− }
1033−
1034 function waitUntilDashIsFocused() {
1043 function waitUntilDashIsFocused() {
1035 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1044 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1036 }
1045 }
@@ -1087,9 +1096,9 @@ Rectangle {
1087 swipeAwayGreeter();
1096 swipeAwayGreeter();
1088 var greeter = findChild(shell, "greeter");
1097 var greeter = findChild(shell, "greeter");
1089
1098
1099+ var appSurfaceId = topLevelSurfaceList.nextId;
1090 var app = ApplicationManager.startApplication("dialer-app");
1100 var app = ApplicationManager.startApplication("dialer-app");
1091− // wait until the app is fully loaded (ie, real surface replaces splash screen)
1101+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
1092− tryCompareFunction(function() { return app.session !== null && app.session.lastSurface !== null }, true);
1093
1102
1094 // Minimize the application we just launched
1103 // Minimize the application we just launched
1095 swipeFromLeftEdge(shell.width * 0.75);
1104 swipeFromLeftEdge(shell.width * 0.75);
@@ -1150,13 +1159,17 @@ Rectangle {
1150 swipeAwayGreeter();
1159 swipeAwayGreeter();
1151 var panel = findChild(shell, "panel");
1160 var panel = findChild(shell, "panel");
1152 compare(panel.fullscreenMode, false);
1161 compare(panel.fullscreenMode, false);
1162+ var cameraSurfaceId = topLevelSurfaceList.nextId;
1153− ApplicationManager.startApplication("camera-app");
1163+ var cameraApp = ApplicationManager.startApplication("camera-app");
1164+ waitUntilAppWindowIsFullyLoaded(cameraSurfaceId);
1154 tryCompare(panel, "fullscreenMode", true);
1165 tryCompare(panel, "fullscreenMode", true);
1166+ var dialerSurfaceId = topLevelSurfaceList.nextId;
1155− ApplicationManager.startApplication("dialer-app");
1167+ var dialerApp = ApplicationManager.startApplication("dialer-app");
1168+ waitUntilAppWindowIsFullyLoaded(dialerSurfaceId);
1156 tryCompare(panel, "fullscreenMode", false);
1169 tryCompare(panel, "fullscreenMode", false);
1157− ApplicationManager.requestFocusApplication("camera-app");
1170+ ApplicationManager.requestFocusApplication(cameraApp.appId);
1158 tryCompare(panel, "fullscreenMode", true);
1171 tryCompare(panel, "fullscreenMode", true);
1159− ApplicationManager.requestFocusApplication("dialer-app");
1172+ ApplicationManager.requestFocusApplication(dialerApp.appId);
1160 tryCompare(panel, "fullscreenMode", false);
1173 tryCompare(panel, "fullscreenMode", false);
1161 }
1174 }
1162
1175
@@ -1227,24 +1240,32 @@ Rectangle {
1227 var tutorial = findChild(shell, "tutorial");
1240 var tutorial = findChild(shell, "tutorial");
1228 tryCompare(wizard, "active", true);
1241 tryCompare(wizard, "active", true);
1229 tryCompare(tutorial, "running", true);
1242 tryCompare(tutorial, "running", true);
1230− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1243+ tryCompareFunction(function() { return topLevelSurfaceList.applicationAt(0).appId; }, "unity8-dash");
1231
1244
1232− // Make sure we stay running when nothing focused (can happen for
1245+ // Make sure we stay running when there's no top level window (can happen for
1233 // a moment when we restart the dash after switching language)
1246 // a moment when we restart the dash after switching language)
1234− ApplicationManager.stopApplication("unity8-dash");
1247+ var dashApplication = ApplicationManager.findApplication("unity8-dash");
1235− tryCompare(ApplicationManager, "focusedApplicationId", "");
1248+ ApplicationManager.stopApplication(dashApplication.appId);
1249+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
1250+ // They won't be gone until those surface items are destroyed.
1251+ tryCompareFunction(function() { return dashApplication.surfaceList.count }, 0);
1252+
1253+ tryCompare(topLevelSurfaceList, "count", 0);
1236 compare(wizard.shown, true);
1254 compare(wizard.shown, true);
1237 compare(tutorial.running, true);
1255 compare(tutorial.running, true);
1238
1256
1239− // And make sure we stay running when dash focused again
1257+ // And make sure we stay running when dash comes back again
1258+ var dashSurfaceId = topLevelSurfaceList.nextId;
1240− ApplicationManager.startApplication("unity8-dash");
1259+ ApplicationManager.startApplication(dashApplication.appId);
1241− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1260+ waitUntilAppWindowIsFullyLoaded(dashSurfaceId);
1242 compare(wizard.shown, true);
1261 compare(wizard.shown, true);
1243 compare(tutorial.running, true);
1262 compare(tutorial.running, true);
1244
1263
1245− // And make sure we stop when something else is focused
1264+ // And make sure we stop when some other surface shows app
1265+ var gallerySurfaceId = topLevelSurfaceList.nextId;
1246− ApplicationManager.startApplication("gallery-app");
1266+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1247− tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
1267+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
1268+ tryCompareFunction(function() { return topLevelSurfaceList.applicationAt(0).appId; }, "gallery-app");
1248 compare(wizard.shown, false);
1269 compare(wizard.shown, false);
1249 compare(tutorial.running, false);
1270 compare(tutorial.running, false);
1250 tryCompare(AccountsService, "demoEdges", false);
1271 tryCompare(AccountsService, "demoEdges", false);
@@ -1269,7 +1290,7 @@ Rectangle {
1269 function test_tapOnRightEdgeReachesApplicationSurface() {
1290 function test_tapOnRightEdgeReachesApplicationSurface() {
1270 loadShell("phone");
1291 loadShell("phone");
1271 swipeAwayGreeter();
1292 swipeAwayGreeter();
1272− var topmostSpreadDelegate = findChild(shell, "appDelegate0");
1293+ var topmostSpreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
1273 verify(topmostSpreadDelegate);
1294 verify(topmostSpreadDelegate);
1274
1295
1275 waitUntilFocusedApplicationIsShowingItsSurface();
1296 waitUntilFocusedApplicationIsShowingItsSurface();
@@ -1298,7 +1319,7 @@ Rectangle {
1298 function test_rightEdgeDragDoesNotReachApplicationSurface() {
1319 function test_rightEdgeDragDoesNotReachApplicationSurface() {
1299 loadShell("phone");
1320 loadShell("phone");
1300 swipeAwayGreeter();
1321 swipeAwayGreeter();
1301− var topmostSpreadDelegate = findChild(shell, "appDelegate0");
1322+ var topmostSpreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
1302 var topmostSurfaceItem = findChild(topmostSpreadDelegate, "surfaceItem");
1323 var topmostSurfaceItem = findChild(topmostSpreadDelegate, "surfaceItem");
1303 var rightEdgeDragArea = findChild(shell, "spreadDragArea");
1324 var rightEdgeDragArea = findChild(shell, "spreadDragArea");
1304
1325
@@ -1318,7 +1339,7 @@ Rectangle {
1318
1339
1319 function waitUntilFocusedApplicationIsShowingItsSurface()
1340 function waitUntilFocusedApplicationIsShowingItsSurface()
1320 {
1341 {
1321− var spreadDelegate = findChild(shell, "appDelegate0");
1342+ var spreadDelegate = findChild(shell, "spreadDelegate_" + topLevelSurfaceList.idAt(0));
1322 var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
1343 var appState = findInvisibleChild(spreadDelegate, "applicationWindowStateGroup");
1323 tryCompare(appState, "state", "surface");
1344 tryCompare(appState, "state", "surface");
1324 var transitions = appState.transitions;
1345 var transitions = appState.transitions;
@@ -1467,11 +1488,13 @@ Rectangle {
1467 loadShell("tablet");
1488 loadShell("tablet");
1468 shell.usageScenario = "desktop";
1489 shell.usageScenario = "desktop";
1469
1490
1491+ var webBrowserSurfaceId = topLevelSurfaceList.nextId;
1470 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1492 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1471− waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1493+ waitUntilAppWindowIsFullyLoaded(webBrowserSurfaceId);
1472
1494
1495+ var gallerySurfaceId = topLevelSurfaceList.nextId;
1473 var galleryApp = ApplicationManager.startApplication("gallery-app");
1496 var galleryApp = ApplicationManager.startApplication("gallery-app");
1474− waitUntilAppWindowIsFullyLoaded(galleryApp);
1497+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
1475
1498
1476 ApplicationManager.requestFocusApplication("unity8-dash");
1499 ApplicationManager.requestFocusApplication("unity8-dash");
1477 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1500 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
@@ -1489,11 +1512,13 @@ Rectangle {
1489 loadShell("tablet");
1512 loadShell("tablet");
1490 shell.usageScenario = "tablet";
1513 shell.usageScenario = "tablet";
1491
1514
1515+ var webBrowserSurfaceId = topLevelSurfaceList.nextId;
1492 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1516 var webBrowserApp = ApplicationManager.startApplication("webbrowser-app");
1493− waitUntilAppWindowIsFullyLoaded(webBrowserApp);
1517+ waitUntilAppWindowIsFullyLoaded(webBrowserSurfaceId);
1494
1518
1519+ var gallerySurfaceId = topLevelSurfaceList.nextId;
1495 var galleryApp = ApplicationManager.startApplication("gallery-app");
1520 var galleryApp = ApplicationManager.startApplication("gallery-app");
1496− waitUntilAppWindowIsFullyLoaded(galleryApp);
1521+ waitUntilAppWindowIsFullyLoaded(gallerySurfaceId);
1497
1522
1498 ApplicationManager.requestFocusApplication("unity8-dash");
1523 ApplicationManager.requestFocusApplication("unity8-dash");
1499 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
1524 tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
@@ -1523,17 +1548,26 @@ Rectangle {
1523 var desktopStage = findChild(shell, "stage");
1548 var desktopStage = findChild(shell, "stage");
1524 verify(desktopStage != null)
1549 verify(desktopStage != null)
1525
1550
1551+ var app1SurfaceId = topLevelSurfaceList.nextId;
1526 var app1 = ApplicationManager.startApplication("dialer-app")
1552 var app1 = ApplicationManager.startApplication("dialer-app")
1527− waitUntilAppWindowIsFullyLoaded(app1);
1553+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
1554+
1555+ var app2SurfaceId = topLevelSurfaceList.nextId;
1528 var app2 = ApplicationManager.startApplication("webbrowser-app")
1556 var app2 = ApplicationManager.startApplication("webbrowser-app")
1529− waitUntilAppWindowIsFullyLoaded(app2);
1557+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
1558+ var app2Surface = app2.surfaceList.get(0);
1559+ verify(app2Surface);
1560+
1561+ var app3SurfaceId = topLevelSurfaceList.nextId;
1530 var app3 = ApplicationManager.startApplication("camera-app")
1562 var app3 = ApplicationManager.startApplication("camera-app")
1531− waitUntilAppWindowIsFullyLoaded(app3);
1563+ waitUntilAppWindowIsFullyLoaded(app3SurfaceId);
1564+ var app3Surface = app3.surfaceList.get(0);
1565+ verify(app3Surface);
1532
1566
1533 // Do a quick alt-tab and see if focus changes
1567 // Do a quick alt-tab and see if focus changes
1534− tryCompare(app3.session.lastSurface, "activeFocus", true)
1568+ tryCompare(app3Surface, "activeFocus", true)
1535 keyClick(Qt.Key_Tab, Qt.AltModifier)
1569 keyClick(Qt.Key_Tab, Qt.AltModifier)
1536− tryCompare(app2.session.lastSurface, "activeFocus", true)
1570+ tryCompare(app2Surface, "activeFocus", true)
1537
1571
1538 // Press Alt+Tab
1572 // Press Alt+Tab
1539 keyPress(Qt.Key_Alt);
1573 keyPress(Qt.Key_Alt);
@@ -1541,7 +1575,7 @@ Rectangle {
1541 keyRelease(Qt.Key_Alt)
1575 keyRelease(Qt.Key_Alt)
1542
1576
1543 // Focus should have switched back now
1577 // Focus should have switched back now
1544− tryCompare(app3.session.lastSurface, "activeFocus", true)
1578+ tryCompare(app3Surface, "activeFocus", true)
1545 }
1579 }
1546
1580
1547 function test_altTabWrapAround() {
1581 function test_altTabWrapAround() {
@@ -1590,7 +1624,9 @@ Rectangle {
1590 tryCompare(desktopSpread, "state", "")
1624 tryCompare(desktopSpread, "state", "")
1591
1625
1592 // Make sure that after wrapping around once, we have the same one focused as at the beginning
1626 // Make sure that after wrapping around once, we have the same one focused as at the beginning
1593− tryCompare(focused.session.lastSurface, "activeFocus", true)
1627+ var focusedAppSurface = focused.surfaceList.get(0);
1628+ verify(focusedAppSurface);
1629+ tryCompare(focusedAppSurface, "activeFocus", true)
1594 }
1630 }
1595
1631
1596 function test_altBackTabNavigation() {
1632 function test_altBackTabNavigation() {
@@ -1624,7 +1660,7 @@ Rectangle {
1624 keyRelease(Qt.Key_Alt);
1660 keyRelease(Qt.Key_Alt);
1625 }
1661 }
1626
1662
1627− function test_highlightFollowsMouse() {
1663+ function otest_highlightFollowsMouse() {
1628 loadDesktopShellWithApps()
1664 loadDesktopShellWithApps()
1629
1665
1630 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1666 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
@@ -1661,9 +1697,7 @@ Rectangle {
1661 keyPress(Qt.Key_Alt)
1697 keyPress(Qt.Key_Alt)
1662 keyClick(Qt.Key_Tab);
1698 keyClick(Qt.Key_Tab);
1663
1699
1664− appRemovedSpy.clear();
1700+ var surfaceId = topLevelSurfaceList.idAt(2);
1665−
1666− var closedAppId = ApplicationManager.get(2).appId;
1667 var spreadDelegate2 = spreadRepeater.itemAt(2);
1701 var spreadDelegate2 = spreadRepeater.itemAt(2);
1668 var closeMouseArea = findChild(spreadDelegate2, "closeMouseArea");
1702 var closeMouseArea = findChild(spreadDelegate2, "closeMouseArea");
1669
1703
@@ -1678,12 +1712,14 @@ Rectangle {
1678 }
1712 }
1679 tryCompare(closeMouseArea, "enabled", true)
1713 tryCompare(closeMouseArea, "enabled", true)
1680
1714
1715+ var countBeforeClickingCloseButton = topLevelSurfaceList.count;
1716+ verify(topLevelSurfaceList.indexForId(surfaceId) === 2);
1717+
1681 // Close the app using the close button
1718 // Close the app using the close button
1682 mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
1719 mouseClick(closeMouseArea, closeMouseArea.width / 2, closeMouseArea.height / 2)
1683
1720
1684− // Verify applicationRemoved has been emitted correctly
1721+ tryCompare(topLevelSurfaceList, "count", countBeforeClickingCloseButton - 1);
1685− tryCompare(appRemovedSpy, "count", 1)
1722+ verify(topLevelSurfaceList.indexForId(surfaceId) === -1);
1686− compare(appRemovedSpy.signalArguments[0][0], closedAppId);
1687
1723
1688 keyRelease(Qt.Key_Alt);
1724 keyRelease(Qt.Key_Alt);
1689 }
1725 }
@@ -1708,7 +1744,7 @@ Rectangle {
1708 keyPress(Qt.Key_Alt)
1744 keyPress(Qt.Key_Alt)
1709 keyClick(Qt.Key_Tab);
1745 keyClick(Qt.Key_Tab);
1710
1746
1711− var focusAppId = ApplicationManager.get(2).appId;
1747+ var surface = topLevelSurfaceList.surfaceAt(2);
1712 var spreadDelegate2 = spreadRepeater.itemAt(2);
1748 var spreadDelegate2 = spreadRepeater.itemAt(2);
1713 var clippedSpreadDelegate = findChild(spreadDelegate2, "clippedSpreadDelegate");
1749 var clippedSpreadDelegate = findChild(spreadDelegate2, "clippedSpreadDelegate");
1714
1750
@@ -1730,7 +1766,7 @@ Rectangle {
1730
1766
1731 // Verify that we left the spread and app2 is the focused one now
1767 // Verify that we left the spread and app2 is the focused one now
1732 tryCompare(stage, "state", "");
1768 tryCompare(stage, "state", "");
1733− tryCompare(ApplicationManager, "focusedApplicationId", focusAppId);
1769+ tryCompare(surface, "focused", true);
1734
1770
1735 keyRelease(Qt.Key_Alt);
1771 keyRelease(Qt.Key_Alt);
1736 }
1772 }
@@ -1794,7 +1830,7 @@ Rectangle {
1794 function test_workspacePreviewsHighlightedApp() {
1830 function test_workspacePreviewsHighlightedApp() {
1795 loadDesktopShellWithApps()
1831 loadDesktopShellWithApps()
1796
1832
1797− var targetZ = ApplicationManager.count + 1;
1833+ var targetZ = topLevelSurfaceList.count + 1;
1798
1834
1799 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1835 var spreadRepeater = findInvisibleChild(shell, "spreadRepeater");
1800 verify(spreadRepeater !== null);
1836 verify(spreadRepeater !== null);
@@ -1870,7 +1906,8 @@ Rectangle {
1870 loadDesktopShellWithApps();
1906 loadDesktopShellWithApps();
1871
1907
1872 var appRepeater = findChild(shell, "appRepeater")
1908 var appRepeater = findChild(shell, "appRepeater")
1873− var appId = ApplicationManager.get(0).appId;
1909+ var application = topLevelSurfaceList.applicationAt(0);
1910+ var surfaceId = topLevelSurfaceList.idAt(0);
1874 var appDelegate = appRepeater.itemAt(0);
1911 var appDelegate = appRepeater.itemAt(0);
1875 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
1912 var maximizeButton = findChild(appDelegate, "maximizeWindowButton");
1876
1913
@@ -1881,17 +1918,20 @@ Rectangle {
1881 tryCompare(appDelegate, "state", "maximized");
1918 tryCompare(appDelegate, "state", "maximized");
1882 tryCompare(PanelState, "buttonsVisible", true)
1919 tryCompare(PanelState, "buttonsVisible", true)
1883
1920
1884− ApplicationManager.stopApplication(appId);
1921+ ApplicationManager.stopApplication(application.appId);
1885 tryCompare(PanelState, "buttonsVisible", false)
1922 tryCompare(PanelState, "buttonsVisible", false)
1886
1923
1924+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
1925+ // They won't be gone until those surface items are destroyed.
1926+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
1927+
1887− ApplicationManager.startApplication(appId);
1928+ ApplicationManager.startApplication(application.appId);
1888 tryCompare(PanelState, "buttonsVisible", true)
1929 tryCompare(PanelState, "buttonsVisible", true)
1889 }
1930 }
1890
1931
1891 function test_newAppHasValidGeometry() {
1932 function test_newAppHasValidGeometry() {
1892 loadDesktopShellWithApps();
1933 loadDesktopShellWithApps();
1893 var appRepeater = findChild(shell, "appRepeater");
1934 var appRepeater = findChild(shell, "appRepeater");
1894− var appId = ApplicationManager.get(0).appId;
1895 var appDelegate = appRepeater.itemAt(0);
1935 var appDelegate = appRepeater.itemAt(0);
1896
1936
1897 var resizeArea = findChild(appDelegate, "windowResizeArea");
1937 var resizeArea = findChild(appDelegate, "windowResizeArea");
@@ -1908,7 +1948,6 @@ Rectangle {
1908 function test_switchToStagedHidesPanelButtons() {
1948 function test_switchToStagedHidesPanelButtons() {
1909 loadDesktopShellWithApps();
1949 loadDesktopShellWithApps();
1910 var appRepeater = findChild(shell, "appRepeater")
1950 var appRepeater = findChild(shell, "appRepeater")
1911− var appId = ApplicationManager.get(0).appId;
1912 var appDelegate = appRepeater.itemAt(0);
1951 var appDelegate = appRepeater.itemAt(0);
1913 var panelButtons = findChild(shell, "panelWindowControlButtons")
1952 var panelButtons = findChild(shell, "panelWindowControlButtons")
1914
1953
@@ -1925,7 +1964,6 @@ Rectangle {
1925 function test_lockingGreeterHidesPanelButtons() {
1964 function test_lockingGreeterHidesPanelButtons() {
1926 loadDesktopShellWithApps();
1965 loadDesktopShellWithApps();
1927 var appRepeater = findChild(shell, "appRepeater")
1966 var appRepeater = findChild(shell, "appRepeater")
1928− var appId = ApplicationManager.get(0).appId;
1929 var appDelegate = appRepeater.itemAt(0);
1967 var appDelegate = appRepeater.itemAt(0);
1930 var panelButtons = findChild(shell, "panelWindowControlButtons")
1968 var panelButtons = findChild(shell, "panelWindowControlButtons")
1931
1969
@@ -1955,12 +1993,15 @@ Rectangle {
1955 shell.usageScenario = "desktop";
1993 shell.usageScenario = "desktop";
1956 waitForRendering(shell);
1994 waitForRendering(shell);
1957
1995
1996+ var appSurfaceId = topLevelSurfaceList.nextId;
1958 var app = ApplicationManager.startApplication("dialer-app")
1997 var app = ApplicationManager.startApplication("dialer-app")
1959− waitUntilAppWindowIsFullyLoaded(app);
1998+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
1960
1999
1961 var appContainer = findChild(shell, "appContainer");
2000 var appContainer = findChild(shell, "appContainer");
2001+ verify(appContainer);
1962− var appDelegate = findChild(appContainer, "appDelegate_dialer-app");
2002+ var appDelegate = findChild(appContainer, "appDelegate_" + appSurfaceId);
2003+ verify(appDelegate);
1963− var decoration = findChild(appDelegate, "appWindowDecoration_dialer-app");
2004+ var decoration = findChild(appDelegate, "appWindowDecoration");
1964 verify(decoration);
2005 verify(decoration);
1965
2006
1966 // move it away from launcher and panel
2007 // move it away from launcher and panel
@@ -1978,18 +2019,22 @@ Rectangle {
1978 function test_restoreWindowStateFixesIfUnderPanel() {
2019 function test_restoreWindowStateFixesIfUnderPanel() {
1979 loadDesktopShellWithApps();
2020 loadDesktopShellWithApps();
1980 var appRepeater = findChild(shell, "appRepeater")
2021 var appRepeater = findChild(shell, "appRepeater")
1981− var appId = ApplicationManager.get(0).appId;
2022+ var application = topLevelSurfaceList.applicationAt(0);
1982 var appDelegate = appRepeater.itemAt(0);
2023 var appDelegate = appRepeater.itemAt(0);
1983
2024
1984 // Move it under the panel programmatically (might happen later with an alt+drag)
2025 // Move it under the panel programmatically (might happen later with an alt+drag)
1985 appDelegate.y = -units.gu(10)
2026 appDelegate.y = -units.gu(10)
1986
2027
1987− ApplicationManager.stopApplication(appId)
2028+ ApplicationManager.stopApplication(application.appId);
2029+ // wait until all zombie surfaces are gone. As MirSurfaceItems hold references over them.
2030+ // They won't be gone until those surface items are destroyed.
2031+ tryCompareFunction(function() { return application.surfaceList.count }, 0);
2032+
1988− ApplicationManager.startApplication(appId)
2033+ ApplicationManager.startApplication(application.appId);
1989 waitForRendering(shell)
2034 waitForRendering(shell)
1990
2035
1991 // Make sure the newly started one is at index 0 again
2036 // Make sure the newly started one is at index 0 again
1992− tryCompare(ApplicationManager.get(0), "appId", appId);
2037+ tryCompareFunction(function () { return topLevelSurfaceList.applicationAt(0).appId; }, application.appId);
1993
2038
1994 appDelegate = appRepeater.itemAt(0);
2039 appDelegate = appRepeater.itemAt(0);
1995 compare(appDelegate.y >= PanelState.panelHeight, true);
2040 compare(appDelegate.y >= PanelState.panelHeight, true);
@@ -2008,16 +2053,22 @@ Rectangle {
2008
2053
2009 // Add two main stage apps, the second in order to suspend the first.
2054 // Add two main stage apps, the second in order to suspend the first.
2010 // LibreOffice has isTouchApp set to false by our mocks.
2055 // LibreOffice has isTouchApp set to false by our mocks.
2056+ var app1SurfaceId = topLevelSurfaceList.nextId;
2011 var app1 = ApplicationManager.startApplication("libreoffice");
2057 var app1 = ApplicationManager.startApplication("libreoffice");
2012− waitUntilAppWindowIsFullyLoaded(app1);
2058+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
2059+ var app2SurfaceId = topLevelSurfaceList.nextId;
2013 var app2 = ApplicationManager.startApplication("gallery-app");
2060 var app2 = ApplicationManager.startApplication("gallery-app");
2014− waitUntilAppWindowIsFullyLoaded(app2);
2061+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
2015
2062
2016 // Sanity checking
2063 // Sanity checking
2017 compare(app1.stage, ApplicationInfoInterface.MainStage);
2064 compare(app1.stage, ApplicationInfoInterface.MainStage);
2018 compare(app2.stage, ApplicationInfoInterface.MainStage);
2065 compare(app2.stage, ApplicationInfoInterface.MainStage);
2019 verify(!app1.isTouchApp);
2066 verify(!app1.isTouchApp);
2067+
2068+ var app1Surface = app1.surfaceList.get(0);
2069+ verify(app1Surface);
2070+
2020− verify(!app1.session.lastSurface.activeFocus);
2071+ verify(!app1Surface.activeFocus);
2021
2072
2022 // Make sure app1 is exempt with a requested suspend
2073 // Make sure app1 is exempt with a requested suspend
2023 verify(app1.exemptFromLifecycle);
2074 verify(app1.exemptFromLifecycle);
@@ -2038,15 +2089,21 @@ Rectangle {
2038 GSettingsController.setLifecycleExemptAppids(["webbrowser-app"]);
2089 GSettingsController.setLifecycleExemptAppids(["webbrowser-app"]);
2039
2090
2040 // Add two main stage apps, the second in order to suspend the first
2091 // Add two main stage apps, the second in order to suspend the first
2092+ var app1SurfaceId = topLevelSurfaceList.nextId;
2041 var app1 = ApplicationManager.startApplication("webbrowser-app");
2093 var app1 = ApplicationManager.startApplication("webbrowser-app");
2042− waitUntilAppWindowIsFullyLoaded(app1);
2094+ waitUntilAppWindowIsFullyLoaded(app1SurfaceId);
2095+ var app2SurfaceId = topLevelSurfaceList.nextId;
2043 var app2 = ApplicationManager.startApplication("gallery-app");
2096 var app2 = ApplicationManager.startApplication("gallery-app");
2044− waitUntilAppWindowIsFullyLoaded(app2);
2097+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
2045
2098
2046 // Sanity checking
2099 // Sanity checking
2047 compare(app1.stage, ApplicationInfoInterface.MainStage);
2100 compare(app1.stage, ApplicationInfoInterface.MainStage);
2048 compare(app2.stage, ApplicationInfoInterface.MainStage);
2101 compare(app2.stage, ApplicationInfoInterface.MainStage);
2102+
2103+ var app1Surface = app1.surfaceList.get(0);
2104+ verify(app1Surface);
2105+
2049− verify(!app1.session.lastSurface.activeFocus);
2106+ verify(!app1Surface.activeFocus);
2050
2107
2051 // Make sure app1 is exempt with a requested suspend
2108 // Make sure app1 is exempt with a requested suspend
2052 verify(app1.exemptFromLifecycle);
2109 verify(app1.exemptFromLifecycle);
@@ -2205,9 +2262,10 @@ Rectangle {
2205 var appContainer = findChild(shell, "appContainer");
2262 var appContainer = findChild(shell, "appContainer");
2206 var launcher = findChild(shell, "launcher");
2263 var launcher = findChild(shell, "launcher");
2207
2264
2265+ var appSurfaceId = topLevelSurfaceList.nextId;
2208 var app = ApplicationManager.startApplication("music-app");
2266 var app = ApplicationManager.startApplication("music-app");
2209− waitUntilAppWindowIsFullyLoaded(app);
2267+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
2210− var appDelegate = findChild(appContainer, "appDelegate_music-app");
2268+ var appDelegate = findChild(appContainer, "appDelegate_" + appSurfaceId);
2211 appDelegate.maximize();
2269 appDelegate.maximize();
2212 tryCompare(appDelegate, "visuallyMaximized", true);
2270 tryCompare(appDelegate, "visuallyMaximized", true);
2213 waitForRendering(shell);
2271 waitForRendering(shell);
@@ -2235,8 +2293,18 @@ Rectangle {
2235
2293
2236 tryCompare(launcher, "lockedVisible", true);
2294 tryCompare(launcher, "lockedVisible", true);
2237
2295
2296+ var surfaceId = topLevelSurfaceList.nextId;
2238− var cameraApp = ApplicationManager.startApplication("camera-app");
2297+ var app = ApplicationManager.startApplication("gmail-webapp");
2298+
2239− waitUntilAppWindowIsFullyLoaded(cameraApp);
2299+ waitUntilAppWindowIsFullyLoaded(surfaceId);
2300+
2301+ // Sanity check: ensure the fake app we chose creates a surface the way
2302+ // we expect it to.
2303+ compare(app.surfaceList.get(0).shellChrome, Mir.NormalChrome);
2304+
2305+ compare(launcher.lockedVisible, true);
2306+
2307+ app.surfaceList.get(0).state = Mir.FullscreenState;
2240
2308
2241 tryCompare(launcher, "lockedVisible", false);
2309 tryCompare(launcher, "lockedVisible", false);
2242 }
2310 }
@@ -2257,8 +2325,9 @@ Rectangle {
2257 swipeAwayGreeter();
2325 swipeAwayGreeter();
2258
2326
2259 // Let's open a fullscreen app
2327 // Let's open a fullscreen app
2328+ var appSurfaceId = topLevelSurfaceList.nextId;
2260 var app = ApplicationManager.startApplication("camera-app");
2329 var app = ApplicationManager.startApplication("camera-app");
2261− waitUntilAppWindowIsFullyLoaded(app);
2330+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
2262
2331
2263 var appRepeater = findChild(shell, data.repeaterName);
2332 var appRepeater = findChild(shell, data.repeaterName);
2264 var topmostAppDelegate = appRepeater.itemAt(0);
2333 var topmostAppDelegate = appRepeater.itemAt(0);
@@ -2295,19 +2364,21 @@ Rectangle {
2295 AccountsService.keymaps = ["sk", "cz+qwerty", "fr"] // "configure" the keymaps for user
2364 AccountsService.keymaps = ["sk", "cz+qwerty", "fr"] // "configure" the keymaps for user
2296
2365
2297 // start some app
2366 // start some app
2367+ var appSurfaceId = topLevelSurfaceList.nextId;
2298 var app = ApplicationManager.startApplication("dialer-app");
2368 var app = ApplicationManager.startApplication("dialer-app");
2299− waitUntilAppWindowIsFullyLoaded(app);
2369+ waitUntilAppWindowIsFullyLoaded(appSurfaceId);
2370+ var appSurface = app.surfaceList.get(0);
2300
2371
2301 // verify the initial keymap of the newly started app is the first one from the list
2372 // verify the initial keymap of the newly started app is the first one from the list
2302− tryCompare(app.session.lastSurface, "keymap", "sk");
2373+ tryCompare(appSurface, "keymap", "sk");
2303
2374
2304 // switch to next keymap, should go to "cz+qwerty"
2375 // switch to next keymap, should go to "cz+qwerty"
2305 keyClick(Qt.Key_Space, Qt.MetaModifier);
2376 keyClick(Qt.Key_Space, Qt.MetaModifier);
2306− tryCompare(app.session.lastSurface, "keymap", "cz+qwerty");
2377+ tryCompare(appSurface, "keymap", "cz+qwerty");
2307
2378
2308 // switch to next keymap, should go to "fr"
2379 // switch to next keymap, should go to "fr"
2309 keyClick(Qt.Key_Space, Qt.MetaModifier);
2380 keyClick(Qt.Key_Space, Qt.MetaModifier);
2310− tryCompare(app.session.lastSurface, "keymap", "fr");
2381+ tryCompare(appSurface, "keymap", "fr");
2311
2382
2312 // go to e.g. desktop stage
2383 // go to e.g. desktop stage
2313 loadShell("desktop");
2384 loadShell("desktop");
@@ -2315,30 +2386,32 @@ Rectangle {
2315 waitForRendering(shell);
2386 waitForRendering(shell);
2316
2387
2317 // start a second app, should get the last configured keyboard, "fr"
2388 // start a second app, should get the last configured keyboard, "fr"
2389+ var app2SurfaceId = topLevelSurfaceList.nextId;
2318 var app2 = ApplicationManager.startApplication("calendar-app");
2390 var app2 = ApplicationManager.startApplication("calendar-app");
2319− waitUntilAppWindowIsFullyLoaded(app2);
2391+ waitUntilAppWindowIsFullyLoaded(app2SurfaceId);
2392+ var app2Surface = app2.surfaceList.get(0);
2320− tryCompare(app2.session.lastSurface, "keymap", "fr");
2393+ tryCompare(app2Surface, "keymap", "fr");
2321
2394
2322 // focus our first app, make sure it also has the "fr" keymap
2395 // focus our first app, make sure it also has the "fr" keymap
2323 ApplicationManager.requestFocusApplication("dialer-app");
2396 ApplicationManager.requestFocusApplication("dialer-app");
2324− tryCompare(app.session.lastSurface, "keymap", "fr");
2397+ tryCompare(appSurface, "keymap", "fr");
2325
2398
2326 // switch to previous keymap, should be "cz+qwerty"
2399 // switch to previous keymap, should be "cz+qwerty"
2327 keyClick(Qt.Key_Space, Qt.MetaModifier|Qt.ShiftModifier);
2400 keyClick(Qt.Key_Space, Qt.MetaModifier|Qt.ShiftModifier);
2328− tryCompare(app.session.lastSurface, "keymap", "cz+qwerty");
2401+ tryCompare(appSurface, "keymap", "cz+qwerty");
2329
2402
2330 // go next twice to "sk", past the end
2403 // go next twice to "sk", past the end
2331 keyClick(Qt.Key_Space, Qt.MetaModifier);
2404 keyClick(Qt.Key_Space, Qt.MetaModifier);
2332 keyClick(Qt.Key_Space, Qt.MetaModifier);
2405 keyClick(Qt.Key_Space, Qt.MetaModifier);
2333− tryCompare(app.session.lastSurface, "keymap", "sk");
2406+ tryCompare(appSurface, "keymap", "sk");
2334
2407
2335 // go back once to past the beginning, to "fr"
2408 // go back once to past the beginning, to "fr"
2336 keyClick(Qt.Key_Space, Qt.MetaModifier|Qt.ShiftModifier);
2409 keyClick(Qt.Key_Space, Qt.MetaModifier|Qt.ShiftModifier);
2337− tryCompare(app.session.lastSurface, "keymap", "fr");
2410+ tryCompare(appSurface, "keymap", "fr");
2338
2411
2339 // switch to app2, should also get "fr"
2412 // switch to app2, should also get "fr"
2340 ApplicationManager.requestFocusApplication("calendar-app");
2413 ApplicationManager.requestFocusApplication("calendar-app");
2341− tryCompare(app2.session.lastSurface, "keymap", "fr");
2414+ tryCompare(app2Surface, "keymap", "fr");
2342 }
2415 }
2343 }
2416 }
2344}
2417}
tests/qmltests/tst_ShellWithPin.qml+9−13
@@ -197,14 +197,6 @@ Item {
197 removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
197 removeTimeConstraintsFromDirectionalDragAreas(shellLoader.item)
198 }
198 }
199
199
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−
208 function swipeAwayGreeter(waitForCoverPage) {
200 function swipeAwayGreeter(waitForCoverPage) {
209 var greeter = findChild(shell, "greeter");
201 var greeter = findChild(shell, "greeter");
210 waitForRendering(greeter)
202 waitForRendering(greeter)
@@ -318,6 +310,7 @@ Item {
318 var emergencyButton = findChild(greeter, "emergencyCallLabel");
310 var emergencyButton = findChild(greeter, "emergencyCallLabel");
319 tap(emergencyButton)
311 tap(emergencyButton)
320
312
313+
321 tryCompare(greeter, "shown", false);
314 tryCompare(greeter, "shown", false);
322 killApps() // kill dialer-app, as if it crashed
315 killApps() // kill dialer-app, as if it crashed
323 tryCompare(greeter, "shown", true);
316 tryCompare(greeter, "shown", true);
@@ -424,12 +417,15 @@ Item {
424
417
425 var greeter = findChild(shell, "greeter");
418 var greeter = findChild(shell, "greeter");
426 var lockscreen = findChild(shell, "lockscreen");
419 var lockscreen = findChild(shell, "lockscreen");
420+ verify(lockscreen);
427
421
428 lockscreen.emergencyCall();
422 lockscreen.emergencyCall();
429 confirmLockedApp("dialer-app");
423 confirmLockedApp("dialer-app");
430 callManager.foregroundCall = phoneCall;
424 callManager.foregroundCall = phoneCall;
431
425
432 LightDM.Greeter.showGreeter();
426 LightDM.Greeter.showGreeter();
427+ lockscreen = findChild(shell, "lockscreen");
428+ verify(lockscreen);
433 tryCompare(lockscreen, "shown", true);
429 tryCompare(lockscreen, "shown", true);
434 tryCompare(greeter, "hasLockedApp", false);
430 tryCompare(greeter, "hasLockedApp", false);
435
431
@@ -476,7 +472,7 @@ Item {
476
472
477 var app = ApplicationManager.startApplication("gallery-app");
473 var app = ApplicationManager.startApplication("gallery-app");
478 // wait until the app is fully loaded (ie, real surface replaces splash screen)
474 // 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);
480
476
481 // New app hides coverPage?
477 // New app hides coverPage?
482 var greeter = findChild(shell, "greeter");
478 var greeter = findChild(shell, "greeter");
@@ -593,8 +589,8 @@ Item {
593 var coverPage = findChild(shell, "coverPage");
589 var coverPage = findChild(shell, "coverPage");
594 var lockscreen = findChild(shell, "lockscreen");
590 var lockscreen = findChild(shell, "lockscreen");
595 var launcher = findChild(shell, "launcherPanel");
591 var launcher = findChild(shell, "launcherPanel");
596− ApplicationManager.startApplication("gallery-app");
592+ var galleryApp = ApplicationManager.startApplication("gallery-app");
597− tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
593+ tryCompare(shell, "mainApp", galleryApp);
598
594
599 // Show greeter
595 // Show greeter
600 LightDM.Greeter.showGreeter();
596 LightDM.Greeter.showGreeter();
@@ -605,13 +601,13 @@ Item {
605 tryCompare(launcher, "x", -launcher.width);
601 tryCompare(launcher, "x", -launcher.width);
606 tryCompare(coverPage, "showProgress", 0);
602 tryCompare(coverPage, "showProgress", 0);
607 compare(lockscreen.shown, true);
603 compare(lockscreen.shown, true);
608− compare(ApplicationManager.focusedApplicationId, "gallery-app");
604+ tryCompare(shell, "mainApp", galleryApp);
609
605
610 // Now attempt a swipe on lockscreen
606 // Now attempt a swipe on lockscreen
611 touchFlick(shell, 2, shell.height / 2, units.gu(30), shell.height / 2);
607 touchFlick(shell, 2, shell.height / 2, units.gu(30), shell.height / 2);
612 tryCompare(launcher, "x", 0);
608 tryCompare(launcher, "x", 0);
613 compare(lockscreen.shown, true);
609 compare(lockscreen.shown, true);
614− compare(ApplicationManager.focusedApplicationId, "gallery-app");
610+ tryCompare(shell, "mainApp", galleryApp);
615 }
611 }
616 }
612 }
617}
613}
tests/utils/modules/Unity/Test/UnityTestCase.qml+20−2
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2013-2015 Canonical Ltd.
2+ * Copyright 2013-2016 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -16,12 +16,14 @@
16
16
17import QtQuick 2.4
17import QtQuick 2.4
18import QtTest 1.0
18import QtTest 1.0
19+import Unity.Application 0.1
19import Ubuntu.Components 1.3
20import Ubuntu.Components 1.3
20import Unity.Test 0.1 as UT
21import Unity.Test 0.1 as UT
22+import Utils 0.1
21
23
22TestCase {
24TestCase {
23 id: testCase
25 id: testCase
24− TestUtil {id:util}
26+ property var util: TestUtil {id:util}
25
27
26 // This is needed for waitForRendering calls to return
28 // This is needed for waitForRendering calls to return
27 // if the watched element already got rendered
29 // if the watched element already got rendered
@@ -538,4 +540,20 @@ TestCase {
538 tryCompare(transition, "running", false, 2000);
540 tryCompare(transition, "running", false, 2000);
539 }
541 }
540 }
542 }
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+ }
541}
559}